Repository: ariga/atlas
Pull request #1166 (2022-09-30 09:07:08)

cmd/atlas/internal/migrate: template based migrate status
```
$ atlas migrate status --env local 
Migration Status: PENDING
  -- Current Version: 2 (1 statements applied)
  -- Next Version:    2 (1 statements left)
  -- Executed Files:  2 (last one partially)
  -- Pending Files:   2

Last migration attempt had errors:
  -- SQL:   asd bleh
  -- ERROR: near "asd": syntax error in a long line  with linebreaks
```

```
$ atlas migrate status --env local --log '{{ table . }}'
+---------+-------------+---------+-------+---------------------------+----------------+--------------------------+----------+
| VERSION | DESCRIPTION | STATUS  | COUNT |        EXECUTED AT        | EXECUTION TIME |          ERROR           |   SQL    |
+---------+-------------+---------+-------+---------------------------+----------------+--------------------------+----------+
|       1 |             | applied | 1/1   | 2022-09-28 10:51:01 +0200 | 1.85774ms      |                          |          |
+---------+-------------+---------+-------+---------------------------+----------------+--------------------------+----------+
|       2 |             | applied | 1/2   | 2022-09-28 10:51:04 +0200 | 1.139969ms     | near "asd": syntax error | asd bleh |
+         +-------------+---------+-------+---------------------------+----------------+--------------------------+----------+
|         |             | pending | 1/2   |                           |                |                          |          |
+---------+-------------+---------+-------+---------------------------+----------------+--------------------------+----------+
|       3 |             | pending |       |                           |                |                          |          |
+---------+-------------+---------+-------+---------------------------+----------------+--------------------------+----------+
```

```
$ atlas migrate status --env local --log '{{ json . "  " }}'
{
  "Env": {
    "Driver": "sqlite3",
    "URL": {
      "Scheme": "sqlite",
      "Opaque": "",
      "User": null,
      "Host": "",
      "Path": "/home/jcl/Code/ariga/atlas/build/file.db",
      "RawPath": "",
      "OmitHost": false,
      "ForceQuery": false,
      "RawQuery": "",
      "Fragment": "",
      "RawFragment": "",
      "DSN": "/home/jcl/Code/ariga/atlas/build/file.db",
      "Schema": "main"
    },
    "Dir": "/home/jcl/Code/ariga/atlas/build/migrations"
  },
  "Available": [
    {
      "Name": "1.sql",
      "Version": "1"
    },
    {
      "Name": "2.sql",
      "Version": "2"
    },
    {
      "Name": "3.sql",
      "Version": "3"
    }
  ],
  "Pending": [
    {
      "Name": "2.sql",
      "Version": "2"
    },
    {
      "Name": "3.sql",
      "Version": "3"
    }
  ],
  "Applied": [
    {
      "Version": "1",
      "Description": "",
      "Type": "applied",
      "Applied": 1,
      "Total": 1,
      "ExecutedAt": "2022-09-28T10:51:01.125445262+02:00",
      "ExecutionTime": 1857740,
      "OperatorVersion": "Atlas CLI - development"
    },
    {
      "Version": "2",
      "Description": "",
      "Type": "applied",
      "Applied": 1,
      "Total": 2,
      "ExecutedAt": "2022-09-28T10:51:04.117059587+02:00",
      "ExecutionTime": 1139969,
      "Error": "near \"asd\": syntax error in a long line\n\nwith linebreaks",
      "ErrorStmt": "asd bleh",
      "OperatorVersion": "Atlas CLI - development"
    }
  ],
  "Current": "2",
  "Next": "2",
  "Count": 1,
  "Total": 2,
  "Status": "PENDING",
  "Error": "near \"asd\": syntax error in a long line  with linebreaks",
  "SQL": "asd bleh"
}

```


Patch:
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index bb2638f9..8a01dbcf 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -14,10 +14,12 @@ require (
 	github.com/manifoldco/promptui v0.9.0
 	github.com/mattn/go-sqlite3 v1.14.14
 	github.com/mitchellh/go-homedir v1.1.0
+	github.com/olekukonko/tablewriter v0.0.5
 	github.com/pingcap/tidb/parser v0.0.0-20220817134052-9709249e523a
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.0
+	golang.org/x/exp v0.0.0-20220428152302-39d4317da171
 	golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57
 )
 
@@ -47,6 +49,7 @@ require (
 	github.com/kr/text v0.2.0 // indirect
 	github.com/mattn/go-colorable v0.1.9 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-runewidth v0.0.9 // indirect
 	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
 	github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
 	github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
@@ -58,7 +61,6 @@ require (
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.18.1 // indirect
-	golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
 	golang.org/x/text v0.3.7 // indirect
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index fdda5d3c..5baecf53 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -1,11 +1,3 @@
-ariga.io/atlas v0.6.4-0.20220830111134-f2d233c99aa1 h1:/8U7e/qiVREcJHKltZptJoa10gltLcUBVlcqU8gk1V8=
-ariga.io/atlas v0.6.4-0.20220830111134-f2d233c99aa1/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
-ariga.io/atlas v0.6.4-0.20220831192411-1e893f70cbe8 h1:BmgIAqhnFoem+8/jhVAwm+YDRdfeMgcrbOxf/6+3Teg=
-ariga.io/atlas v0.6.4-0.20220831192411-1e893f70cbe8/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
-ariga.io/atlas v0.6.5-0.20220907135005-d80f5e6b79cf h1:sqRKqa3gdunWKmv4G7FC0j4ROKJ8rG+6YoMBPTVGlxo=
-ariga.io/atlas v0.6.5-0.20220907135005-d80f5e6b79cf/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
-ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
-ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
 ariga.io/atlas v0.7.1-0.20220925103247-db83ea0cd178 h1:/fhqvnyyi4h5epqhIsDQW9NLkklDnQ0GRgQqfUhl9Pw=
 ariga.io/atlas v0.7.1-0.20220925103247-db83ea0cd178/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -38,15 +30,12 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI=
 github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
-github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
@@ -207,6 +196,8 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
 github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
@@ -227,6 +218,8 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
@@ -384,8 +377,6 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
 golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 7de5d577..d6a5723d 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -19,7 +19,7 @@ import (
 	"time"
 
 	"ariga.io/atlas/cmd/atlas/internal/lint"
-	entmigrate "ariga.io/atlas/cmd/atlas/internal/migrate"
+	cmdmigrate "ariga.io/atlas/cmd/atlas/internal/migrate"
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
@@ -85,6 +85,9 @@ var (
 			GitDir  string // repository working dir
 			GitBase string // branch name to compare with
 		}
+		Status struct {
+			Format string // log formatting
+		}
 	}
 	// MigrateCmd represents the migrate command. It wraps several other sub-commands.
 	MigrateCmd = &cobra.Command{
@@ -307,6 +310,7 @@ func init() {
 	// Status flags.
 	urlFlag(&MigrateFlags.URL, migrateFlagURL, "u", MigrateStatusCmd.Flags())
 	revisionsFlag(MigrateStatusCmd.Flags())
+	MigrateStatusCmd.Flags().StringVarP(&MigrateFlags.Status.Format, migrateFlagLog, "", "", "custom logging using a Go template")
 	// Set flags.
 	urlFlag(&MigrateFlags.URL, migrateFlagURL, "u", MigrateSetCmd.Flags())
 	// Hash flags.
@@ -372,7 +376,7 @@ func CmdMigrateApplyRun(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	if err := rrw.(*entmigrate.EntRevisions).Migrate(cmd.Context()); err != nil {
+	if err := rrw.(*cmdmigrate.EntRevisions).Migrate(cmd.Context()); err != nil {
 		return err
 	}
 	// Determine pending files and lock the database while working.
@@ -479,8 +483,8 @@ schema if it is unused.
 	return nil
 }
 
-func entRevisions(ctx context.Context, c *sqlclient.Client) (*entmigrate.EntRevisions, error) {
-	return entmigrate.NewEntRevisions(ctx, c, entmigrate.WithSchema(revisionSchemaName(c)))
+func entRevisions(ctx context.Context, c *sqlclient.Client) (*cmdmigrate.EntRevisions, error) {
+	return cmdmigrate.NewEntRevisions(ctx, c, cmdmigrate.WithSchema(revisionSchemaName(c)))
 }
 
 // defaultRevisionSchema is the default schema for storing revisions table.
@@ -595,7 +599,7 @@ func executorOptions(l migrate.Logger) []migrate.ExecutorOption {
 
 func operatorVersion() string {
 	v, _ := parse(version)
-	return "Atlas CLI - " + v
+	return "Atlas CLI " + v
 }
 
 // CmdMigrateDiffRun is the command executed when running the CLI with 'migrate diff' args.
@@ -906,16 +910,10 @@ func CmdMigrateSetRun(cmd *cobra.Command, args []string) error {
 
 // CmdMigrateStatusRun is the command executed when running the CLI with 'migrate status' args.
 func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
-	// Open the migration directory.
 	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
-	avail, err := dir.Files()
-	if err != nil {
-		return err
-	}
-	// Open a client to the database.
 	client, err := sqlclient.Open(cmd.Context(), MigrateFlags.URL)
 	if err != nil {
 		return err
@@ -924,87 +922,19 @@ func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
 	if err := checkRevisionSchemaClarity(cmd, client); err != nil {
 		return err
 	}
-	// Inspect schema and check if the table does already exist.
-	s, err := client.InspectSchema(
-		cmd.Context(),
-		revisionSchemaName(client),
-		&schema.InspectOptions{Tables: []string{revision.Table}},
-	)
-	switch {
-	case err != nil && !schema.IsNotExistError(err):
-		return err
-	case schema.IsNotExistError(err):
-		return statusPrint(cmd.OutOrStdout(), avail, avail, nil)
-	}
-	if _, ok := s.Table(revision.Table); !ok {
-		// Table does not exist.
-		return statusPrint(cmd.OutOrStdout(), avail, avail, nil)
-	}
-	// Currently, only in DB revisions are supported.
-	rrw, err := entRevisions(cmd.Context(), client)
-	if err != nil {
-		return err
-	}
-	// Executor can give us insights on the revision state.
-	ex, err := migrate.NewExecutor(client.Driver, dir, rrw)
-	if err != nil {
-		return err
-	}
-	pending, err := ex.Pending(cmd.Context())
-	if err != nil && !errors.Is(err, migrate.ErrNoPendingFiles) {
-		return err
-	}
-	revs, err := rrw.ReadRevisions(cmd.Context())
-	if err != nil {
-		return err
-	}
-	return statusPrint(cmd.OutOrStdout(), avail, pending, revs)
-}
-
-func statusPrint(out io.Writer, avail, pending []migrate.File, revs []*migrate.Revision) (err error) {
-	var (
-		cur, next, state string
-		applied          = avail[: len(avail)-len(pending) : len(avail)-len(pending)]
-		partial          = len(revs) != 0 && revs[len(revs)-1].Applied < revs[len(revs)-1].Total
-	)
-	switch len(pending) {
-	case len(avail):
-		cur = "No version applied yet"
-	default:
-		cur = cyan(applied[len(applied)-1].Version())
-		// If the last pending version is partially applied, tell so.
-		if partial {
-			cur += fmt.Sprintf(" (%d statements applied)", revs[len(revs)-1].Applied)
-		}
-	}
-	if len(pending) == 0 {
-		state = green("OK")
-		next = "Already at latest version"
-	} else {
-		state = yellow("PENDING")
-		next = cyan(pending[0].Version())
-		if partial {
-			next += fmt.Sprintf(" (%d statements left)", revs[len(revs)-1].Total-revs[len(revs)-1].Applied)
+	var format = cmdmigrate.DefaultTemplate
+	if f := MigrateFlags.Status.Format; f != "" {
+		format, err = template.New("format").Funcs(cmdmigrate.TemplateFuncs).Parse(f)
+		if err != nil {
+			return fmt.Errorf("parse log format: %w", err)
 		}
 	}
-	exec := cyan(strconv.Itoa(len(applied)))
-	if partial {
-		exec += " + 1 partially"
-	}
-	c := cyan
-	if len(pending) == 0 {
-		c = green
-	}
-	fmt.Fprintf(out, "Migration Status: %s\n", state)
-	fmt.Fprintf(out, "%s%s Current Version:  %s\n", indent2, dash, cur)
-	fmt.Fprintf(out, "%s%s Next Version:     %s\n", indent2, dash, next)
-	fmt.Fprintf(out, "%s%s Executed Files:   %s\n", indent2, dash, exec)
-	fmt.Fprintf(out, "%s%s Pending Files:    %s", indent2, dash, c(strconv.Itoa(len(pending))))
-	if partial {
-		fmt.Fprintf(out, " (partially)")
-	}
-	fmt.Fprintf(out, "\n")
-	return nil
+	return (&cmdmigrate.Reporter{
+		Client:       client,
+		Dir:          dir,
+		ReportWriter: &cmdmigrate.TemplateWriter{T: format, W: cmd.OutOrStdout()},
+		Schema:       revisionSchemaName(client),
+	}).Status(cmd.Context())
 }
 
 // CmdMigrateValidateRun is the command executed when running the CLI with 'migrate validate' args.
diff --git a/cmd/atlas/internal/lint/run.go b/cmd/atlas/internal/lint/run.go
index b8859ab1..92c0f424 100644
--- a/cmd/atlas/internal/lint/run.go
+++ b/cmd/atlas/internal/lint/run.go
@@ -138,8 +138,19 @@ func (r *Runner) summary(ctx context.Context) error {
 var (
 	// TemplateFuncs are global functions available in templates.
 	TemplateFuncs = template.FuncMap{
-		"json": func(v any) (string, error) {
-			b, err := json.Marshal(v)
+		"json": func(v any, args ...string) (string, error) {
+			var (
+				b   []byte
+				err error
+			)
+			switch len(args) {
+			case 0:
+				b, err = json.Marshal(v)
+			case 1:
+				b, err = json.MarshalIndent(v, "", args[0])
+			default:
+				b, err = json.MarshalIndent(v, args[0], args[1])
+			}
 			return string(b), err
 		},
 	}
diff --git a/cmd/atlas/internal/migrate/ent/convert.go b/cmd/atlas/internal/migrate/ent/convert.go
index 828c2bea..985fff8b 100644
--- a/cmd/atlas/internal/migrate/ent/convert.go
+++ b/cmd/atlas/internal/migrate/ent/convert.go
@@ -18,6 +18,7 @@ func (rc *RevisionCreate) SetRevision(rev *migrate.Revision) *RevisionCreate {
 	rc.SetExecutedAt(rev.ExecutedAt)
 	rc.SetExecutionTime(rev.ExecutionTime)
 	rc.SetError(rev.Error)
+	rc.SetErrorStmt(rev.ErrorStmt)
 	rc.SetHash(rev.Hash)
 	rc.SetPartialHashes(rev.PartialHashes)
 	rc.SetOperatorVersion(rev.OperatorVersion)
@@ -35,6 +36,7 @@ func (r *Revision) AtlasRevision() *migrate.Revision {
 		ExecutedAt:      r.ExecutedAt,
 		ExecutionTime:   r.ExecutionTime,
 		Error:           r.Error,
+		ErrorStmt:       r.ErrorStmt,
 		Hash:            r.Hash,
 		PartialHashes:   r.PartialHashes,
 		OperatorVersion: r.OperatorVersion,
diff --git a/cmd/atlas/internal/migrate/ent/migrate/schema.go b/cmd/atlas/internal/migrate/ent/migrate/schema.go
index 8cdb1ee6..ef045730 100644
--- a/cmd/atlas/internal/migrate/ent/migrate/schema.go
+++ b/cmd/atlas/internal/migrate/ent/migrate/schema.go
@@ -23,6 +23,7 @@ var (
 		{Name: "executed_at", Type: field.TypeTime},
 		{Name: "execution_time", Type: field.TypeInt64},
 		{Name: "error", Type: field.TypeString, Nullable: true, Size: 2147483647},
+		{Name: "error_stmt", Type: field.TypeString, Nullable: true, Size: 2147483647},
 		{Name: "hash", Type: field.TypeString},
 		{Name: "partial_hashes", Type: field.TypeJSON, Nullable: true},
 		{Name: "operator_version", Type: field.TypeString},
diff --git a/cmd/atlas/internal/migrate/ent/mutation.go b/cmd/atlas/internal/migrate/ent/mutation.go
index bf67333e..359b98c9 100644
--- a/cmd/atlas/internal/migrate/ent/mutation.go
+++ b/cmd/atlas/internal/migrate/ent/mutation.go
@@ -49,6 +49,7 @@ type RevisionMutation struct {
 	execution_time    *time.Duration
 	addexecution_time *time.Duration
 	error             *string
+	error_stmt        *string
 	hash              *string
 	partial_hashes    *[]string
 	operator_version  *string
@@ -507,6 +508,55 @@ func (m *RevisionMutation) ResetError() {
 	delete(m.clearedFields, revision.FieldError)
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (m *RevisionMutation) SetErrorStmt(s string) {
+	m.error_stmt = &s
+}
+
+// ErrorStmt returns the value of the "error_stmt" field in the mutation.
+func (m *RevisionMutation) ErrorStmt() (r string, exists bool) {
+	v := m.error_stmt
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldErrorStmt returns the old "error_stmt" field's value of the Revision entity.
+// If the Revision object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *RevisionMutation) OldErrorStmt(ctx context.Context) (v string, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldErrorStmt is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldErrorStmt requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldErrorStmt: %w", err)
+	}
+	return oldValue.ErrorStmt, nil
+}
+
+// ClearErrorStmt clears the value of the "error_stmt" field.
+func (m *RevisionMutation) ClearErrorStmt() {
+	m.error_stmt = nil
+	m.clearedFields[revision.FieldErrorStmt] = struct{}{}
+}
+
+// ErrorStmtCleared returns if the "error_stmt" field was cleared in this mutation.
+func (m *RevisionMutation) ErrorStmtCleared() bool {
+	_, ok := m.clearedFields[revision.FieldErrorStmt]
+	return ok
+}
+
+// ResetErrorStmt resets all changes to the "error_stmt" field.
+func (m *RevisionMutation) ResetErrorStmt() {
+	m.error_stmt = nil
+	delete(m.clearedFields, revision.FieldErrorStmt)
+}
+
 // SetHash sets the "hash" field.
 func (m *RevisionMutation) SetHash(s string) {
 	m.hash = &s
@@ -647,7 +697,7 @@ func (m *RevisionMutation) Type() string {
 // order to get all numeric fields that were incremented/decremented, call
 // AddedFields().
 func (m *RevisionMutation) Fields() []string {
-	fields := make([]string, 0, 10)
+	fields := make([]string, 0, 11)
 	if m.description != nil {
 		fields = append(fields, revision.FieldDescription)
 	}
@@ -669,6 +719,9 @@ func (m *RevisionMutation) Fields() []string {
 	if m.error != nil {
 		fields = append(fields, revision.FieldError)
 	}
+	if m.error_stmt != nil {
+		fields = append(fields, revision.FieldErrorStmt)
+	}
 	if m.hash != nil {
 		fields = append(fields, revision.FieldHash)
 	}
@@ -700,6 +753,8 @@ func (m *RevisionMutation) Field(name string) (ent.Value, bool) {
 		return m.ExecutionTime()
 	case revision.FieldError:
 		return m.Error()
+	case revision.FieldErrorStmt:
+		return m.ErrorStmt()
 	case revision.FieldHash:
 		return m.Hash()
 	case revision.FieldPartialHashes:
@@ -729,6 +784,8 @@ func (m *RevisionMutation) OldField(ctx context.Context, name string) (ent.Value
 		return m.OldExecutionTime(ctx)
 	case revision.FieldError:
 		return m.OldError(ctx)
+	case revision.FieldErrorStmt:
+		return m.OldErrorStmt(ctx)
 	case revision.FieldHash:
 		return m.OldHash(ctx)
 	case revision.FieldPartialHashes:
@@ -793,6 +850,13 @@ func (m *RevisionMutation) SetField(name string, value ent.Value) error {
 		}
 		m.SetError(v)
 		return nil
+	case revision.FieldErrorStmt:
+		v, ok := value.(string)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetErrorStmt(v)
+		return nil
 	case revision.FieldHash:
 		v, ok := value.(string)
 		if !ok {
@@ -898,6 +962,9 @@ func (m *RevisionMutation) ClearedFields() []string {
 	if m.FieldCleared(revision.FieldError) {
 		fields = append(fields, revision.FieldError)
 	}
+	if m.FieldCleared(revision.FieldErrorStmt) {
+		fields = append(fields, revision.FieldErrorStmt)
+	}
 	if m.FieldCleared(revision.FieldPartialHashes) {
 		fields = append(fields, revision.FieldPartialHashes)
 	}
@@ -918,6 +985,9 @@ func (m *RevisionMutation) ClearField(name string) error {
 	case revision.FieldError:
 		m.ClearError()
 		return nil
+	case revision.FieldErrorStmt:
+		m.ClearErrorStmt()
+		return nil
 	case revision.FieldPartialHashes:
 		m.ClearPartialHashes()
 		return nil
@@ -950,6 +1020,9 @@ func (m *RevisionMutation) ResetField(name string) error {
 	case revision.FieldError:
 		m.ResetError()
 		return nil
+	case revision.FieldErrorStmt:
+		m.ResetErrorStmt()
+		return nil
 	case revision.FieldHash:
 		m.ResetHash()
 		return nil
diff --git a/cmd/atlas/internal/migrate/ent/revision.go b/cmd/atlas/internal/migrate/ent/revision.go
index 57269d0d..70a4f0a8 100644
--- a/cmd/atlas/internal/migrate/ent/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision.go
@@ -36,6 +36,8 @@ type Revision struct {
 	ExecutionTime time.Duration `json:"execution_time,omitempty"`
 	// Error holds the value of the "error" field.
 	Error string `json:"error,omitempty"`
+	// ErrorStmt holds the value of the "error_stmt" field.
+	ErrorStmt string `json:"error_stmt,omitempty"`
 	// Hash holds the value of the "hash" field.
 	Hash string `json:"hash,omitempty"`
 	// PartialHashes holds the value of the "partial_hashes" field.
@@ -53,7 +55,7 @@ func (*Revision) scanValues(columns []string) ([]interface{}, error) {
 			values[i] = new([]byte)
 		case revision.FieldType, revision.FieldApplied, revision.FieldTotal, revision.FieldExecutionTime:
 			values[i] = new(sql.NullInt64)
-		case revision.FieldID, revision.FieldDescription, revision.FieldError, revision.FieldHash, revision.FieldOperatorVersion:
+		case revision.FieldID, revision.FieldDescription, revision.FieldError, revision.FieldErrorStmt, revision.FieldHash, revision.FieldOperatorVersion:
 			values[i] = new(sql.NullString)
 		case revision.FieldExecutedAt:
 			values[i] = new(sql.NullTime)
@@ -120,6 +122,12 @@ func (r *Revision) assignValues(columns []string, values []interface{}) error {
 			} else if value.Valid {
 				r.Error = value.String
 			}
+		case revision.FieldErrorStmt:
+			if value, ok := values[i].(*sql.NullString); !ok {
+				return fmt.Errorf("unexpected type %T for field error_stmt", values[i])
+			} else if value.Valid {
+				r.ErrorStmt = value.String
+			}
 		case revision.FieldHash:
 			if value, ok := values[i].(*sql.NullString); !ok {
 				return fmt.Errorf("unexpected type %T for field hash", values[i])
@@ -189,6 +197,9 @@ func (r *Revision) String() string {
 	builder.WriteString("error=")
 	builder.WriteString(r.Error)
 	builder.WriteString(", ")
+	builder.WriteString("error_stmt=")
+	builder.WriteString(r.ErrorStmt)
+	builder.WriteString(", ")
 	builder.WriteString("hash=")
 	builder.WriteString(r.Hash)
 	builder.WriteString(", ")
diff --git a/cmd/atlas/internal/migrate/ent/revision/revision.go b/cmd/atlas/internal/migrate/ent/revision/revision.go
index edc64e36..4f6d29ce 100644
--- a/cmd/atlas/internal/migrate/ent/revision/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision/revision.go
@@ -29,6 +29,8 @@ const (
 	FieldExecutionTime = "execution_time"
 	// FieldError holds the string denoting the error field in the database.
 	FieldError = "error"
+	// FieldErrorStmt holds the string denoting the error_stmt field in the database.
+	FieldErrorStmt = "error_stmt"
 	// FieldHash holds the string denoting the hash field in the database.
 	FieldHash = "hash"
 	// FieldPartialHashes holds the string denoting the partial_hashes field in the database.
@@ -49,6 +51,7 @@ var Columns = []string{
 	FieldExecutedAt,
 	FieldExecutionTime,
 	FieldError,
+	FieldErrorStmt,
 	FieldHash,
 	FieldPartialHashes,
 	FieldOperatorVersion,
diff --git a/cmd/atlas/internal/migrate/ent/revision/where.go b/cmd/atlas/internal/migrate/ent/revision/where.go
index 30b71173..ea660ef9 100644
--- a/cmd/atlas/internal/migrate/ent/revision/where.go
+++ b/cmd/atlas/internal/migrate/ent/revision/where.go
@@ -136,6 +136,13 @@ func Error(v string) predicate.Revision {
 	})
 }
 
+// ErrorStmt applies equality check predicate on the "error_stmt" field. It's identical to ErrorStmtEQ.
+func ErrorStmt(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldErrorStmt), v))
+	})
+}
+
 // Hash applies equality check predicate on the "hash" field. It's identical to HashEQ.
 func Hash(v string) predicate.Revision {
 	return predicate.Revision(func(s *sql.Selector) {
@@ -778,6 +785,131 @@ func ErrorContainsFold(v string) predicate.Revision {
 	})
 }
 
+// ErrorStmtEQ applies the EQ predicate on the "error_stmt" field.
+func ErrorStmtEQ(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtNEQ applies the NEQ predicate on the "error_stmt" field.
+func ErrorStmtNEQ(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.NEQ(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtIn applies the In predicate on the "error_stmt" field.
+func ErrorStmtIn(vs ...string) predicate.Revision {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.Revision(func(s *sql.Selector) {
+		// if not arguments were provided, append the FALSE constants,
+		// since we can't apply "IN ()". This will make this predicate falsy.
+		if len(v) == 0 {
+			s.Where(sql.False())
+			return
+		}
+		s.Where(sql.In(s.C(FieldErrorStmt), v...))
+	})
+}
+
+// ErrorStmtNotIn applies the NotIn predicate on the "error_stmt" field.
+func ErrorStmtNotIn(vs ...string) predicate.Revision {
+	v := make([]interface{}, len(vs))
+	for i := range v {
+		v[i] = vs[i]
+	}
+	return predicate.Revision(func(s *sql.Selector) {
+		// if not arguments were provided, append the FALSE constants,
+		// since we can't apply "IN ()". This will make this predicate falsy.
+		if len(v) == 0 {
+			s.Where(sql.False())
+			return
+		}
+		s.Where(sql.NotIn(s.C(FieldErrorStmt), v...))
+	})
+}
+
+// ErrorStmtGT applies the GT predicate on the "error_stmt" field.
+func ErrorStmtGT(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtGTE applies the GTE predicate on the "error_stmt" field.
+func ErrorStmtGTE(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtLT applies the LT predicate on the "error_stmt" field.
+func ErrorStmtLT(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtLTE applies the LTE predicate on the "error_stmt" field.
+func ErrorStmtLTE(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtContains applies the Contains predicate on the "error_stmt" field.
+func ErrorStmtContains(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.Contains(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtHasPrefix applies the HasPrefix predicate on the "error_stmt" field.
+func ErrorStmtHasPrefix(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.HasPrefix(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtHasSuffix applies the HasSuffix predicate on the "error_stmt" field.
+func ErrorStmtHasSuffix(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.HasSuffix(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtIsNil applies the IsNil predicate on the "error_stmt" field.
+func ErrorStmtIsNil() predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.IsNull(s.C(FieldErrorStmt)))
+	})
+}
+
+// ErrorStmtNotNil applies the NotNil predicate on the "error_stmt" field.
+func ErrorStmtNotNil() predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.NotNull(s.C(FieldErrorStmt)))
+	})
+}
+
+// ErrorStmtEqualFold applies the EqualFold predicate on the "error_stmt" field.
+func ErrorStmtEqualFold(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EqualFold(s.C(FieldErrorStmt), v))
+	})
+}
+
+// ErrorStmtContainsFold applies the ContainsFold predicate on the "error_stmt" field.
+func ErrorStmtContainsFold(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.ContainsFold(s.C(FieldErrorStmt), v))
+	})
+}
+
 // HashEQ applies the EQ predicate on the "hash" field.
 func HashEQ(v string) predicate.Revision {
 	return predicate.Revision(func(s *sql.Selector) {
diff --git a/cmd/atlas/internal/migrate/ent/revision_create.go b/cmd/atlas/internal/migrate/ent/revision_create.go
index babecc35..3e9f6a12 100644
--- a/cmd/atlas/internal/migrate/ent/revision_create.go
+++ b/cmd/atlas/internal/migrate/ent/revision_create.go
@@ -102,6 +102,20 @@ func (rc *RevisionCreate) SetNillableError(s *string) *RevisionCreate {
 	return rc
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (rc *RevisionCreate) SetErrorStmt(s string) *RevisionCreate {
+	rc.mutation.SetErrorStmt(s)
+	return rc
+}
+
+// SetNillableErrorStmt sets the "error_stmt" field if the given value is not nil.
+func (rc *RevisionCreate) SetNillableErrorStmt(s *string) *RevisionCreate {
+	if s != nil {
+		rc.SetErrorStmt(*s)
+	}
+	return rc
+}
+
 // SetHash sets the "hash" field.
 func (rc *RevisionCreate) SetHash(s string) *RevisionCreate {
 	rc.mutation.SetHash(s)
@@ -347,6 +361,14 @@ func (rc *RevisionCreate) createSpec() (*Revision, *sqlgraph.CreateSpec) {
 		})
 		_node.Error = value
 	}
+	if value, ok := rc.mutation.ErrorStmt(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: revision.FieldErrorStmt,
+		})
+		_node.ErrorStmt = value
+	}
 	if value, ok := rc.mutation.Hash(); ok {
 		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -537,6 +559,24 @@ func (u *RevisionUpsert) ClearError() *RevisionUpsert {
 	return u
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (u *RevisionUpsert) SetErrorStmt(v string) *RevisionUpsert {
+	u.Set(revision.FieldErrorStmt, v)
+	return u
+}
+
+// UpdateErrorStmt sets the "error_stmt" field to the value that was provided on create.
+func (u *RevisionUpsert) UpdateErrorStmt() *RevisionUpsert {
+	u.SetExcluded(revision.FieldErrorStmt)
+	return u
+}
+
+// ClearErrorStmt clears the value of the "error_stmt" field.
+func (u *RevisionUpsert) ClearErrorStmt() *RevisionUpsert {
+	u.SetNull(revision.FieldErrorStmt)
+	return u
+}
+
 // SetHash sets the "hash" field.
 func (u *RevisionUpsert) SetHash(v string) *RevisionUpsert {
 	u.Set(revision.FieldHash, v)
@@ -766,6 +806,27 @@ func (u *RevisionUpsertOne) ClearError() *RevisionUpsertOne {
 	})
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (u *RevisionUpsertOne) SetErrorStmt(v string) *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetErrorStmt(v)
+	})
+}
+
+// UpdateErrorStmt sets the "error_stmt" field to the value that was provided on create.
+func (u *RevisionUpsertOne) UpdateErrorStmt() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateErrorStmt()
+	})
+}
+
+// ClearErrorStmt clears the value of the "error_stmt" field.
+func (u *RevisionUpsertOne) ClearErrorStmt() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.ClearErrorStmt()
+	})
+}
+
 // SetHash sets the "hash" field.
 func (u *RevisionUpsertOne) SetHash(v string) *RevisionUpsertOne {
 	return u.Update(func(s *RevisionUpsert) {
@@ -1166,6 +1227,27 @@ func (u *RevisionUpsertBulk) ClearError() *RevisionUpsertBulk {
 	})
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (u *RevisionUpsertBulk) SetErrorStmt(v string) *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetErrorStmt(v)
+	})
+}
+
+// UpdateErrorStmt sets the "error_stmt" field to the value that was provided on create.
+func (u *RevisionUpsertBulk) UpdateErrorStmt() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateErrorStmt()
+	})
+}
+
+// ClearErrorStmt clears the value of the "error_stmt" field.
+func (u *RevisionUpsertBulk) ClearErrorStmt() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.ClearErrorStmt()
+	})
+}
+
 // SetHash sets the "hash" field.
 func (u *RevisionUpsertBulk) SetHash(v string) *RevisionUpsertBulk {
 	return u.Update(func(s *RevisionUpsert) {
diff --git a/cmd/atlas/internal/migrate/ent/revision_update.go b/cmd/atlas/internal/migrate/ent/revision_update.go
index 3e5a9f6a..975f7208 100644
--- a/cmd/atlas/internal/migrate/ent/revision_update.go
+++ b/cmd/atlas/internal/migrate/ent/revision_update.go
@@ -130,6 +130,26 @@ func (ru *RevisionUpdate) ClearError() *RevisionUpdate {
 	return ru
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (ru *RevisionUpdate) SetErrorStmt(s string) *RevisionUpdate {
+	ru.mutation.SetErrorStmt(s)
+	return ru
+}
+
+// SetNillableErrorStmt sets the "error_stmt" field if the given value is not nil.
+func (ru *RevisionUpdate) SetNillableErrorStmt(s *string) *RevisionUpdate {
+	if s != nil {
+		ru.SetErrorStmt(*s)
+	}
+	return ru
+}
+
+// ClearErrorStmt clears the value of the "error_stmt" field.
+func (ru *RevisionUpdate) ClearErrorStmt() *RevisionUpdate {
+	ru.mutation.ClearErrorStmt()
+	return ru
+}
+
 // SetHash sets the "hash" field.
 func (ru *RevisionUpdate) SetHash(s string) *RevisionUpdate {
 	ru.mutation.SetHash(s)
@@ -321,6 +341,19 @@ func (ru *RevisionUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			Column: revision.FieldError,
 		})
 	}
+	if value, ok := ru.mutation.ErrorStmt(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: revision.FieldErrorStmt,
+		})
+	}
+	if ru.mutation.ErrorStmtCleared() {
+		_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Column: revision.FieldErrorStmt,
+		})
+	}
 	if value, ok := ru.mutation.Hash(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -465,6 +498,26 @@ func (ruo *RevisionUpdateOne) ClearError() *RevisionUpdateOne {
 	return ruo
 }
 
+// SetErrorStmt sets the "error_stmt" field.
+func (ruo *RevisionUpdateOne) SetErrorStmt(s string) *RevisionUpdateOne {
+	ruo.mutation.SetErrorStmt(s)
+	return ruo
+}
+
+// SetNillableErrorStmt sets the "error_stmt" field if the given value is not nil.
+func (ruo *RevisionUpdateOne) SetNillableErrorStmt(s *string) *RevisionUpdateOne {
+	if s != nil {
+		ruo.SetErrorStmt(*s)
+	}
+	return ruo
+}
+
+// ClearErrorStmt clears the value of the "error_stmt" field.
+func (ruo *RevisionUpdateOne) ClearErrorStmt() *RevisionUpdateOne {
+	ruo.mutation.ClearErrorStmt()
+	return ruo
+}
+
 // SetHash sets the "hash" field.
 func (ruo *RevisionUpdateOne) SetHash(s string) *RevisionUpdateOne {
 	ruo.mutation.SetHash(s)
@@ -686,6 +739,19 @@ func (ruo *RevisionUpdateOne) sqlSave(ctx context.Context) (_node *Revision, err
 			Column: revision.FieldError,
 		})
 	}
+	if value, ok := ruo.mutation.ErrorStmt(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: revision.FieldErrorStmt,
+		})
+	}
+	if ruo.mutation.ErrorStmtCleared() {
+		_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Column: revision.FieldErrorStmt,
+		})
+	}
 	if value, ok := ruo.mutation.Hash(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
diff --git a/cmd/atlas/internal/migrate/ent/schema/revision.go b/cmd/atlas/internal/migrate/ent/schema/revision.go
index 8d0fe856..13d457f8 100644
--- a/cmd/atlas/internal/migrate/ent/schema/revision.go
+++ b/cmd/atlas/internal/migrate/ent/schema/revision.go
@@ -46,6 +46,8 @@ func (Revision) Fields() []ent.Field {
 			GoType(time.Duration(0)),
 		field.Text("error").
 			Optional(),
+		field.Text("error_stmt").
+			Optional(),
 		field.String("hash"),
 		field.Strings("partial_hashes").
 			Optional(),
diff --git a/cmd/atlas/internal/migrate/migrate.go b/cmd/atlas/internal/migrate/migrate.go
index 36a39279..82e18a3b 100644
--- a/cmd/atlas/internal/migrate/migrate.go
+++ b/cmd/atlas/internal/migrate/migrate.go
@@ -6,6 +6,7 @@ package migrate
 
 import (
 	"context"
+	"fmt"
 
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent"
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
@@ -122,7 +123,7 @@ func (r *EntRevisions) DeleteRevision(ctx context.Context, v string) error {
 // Migrate attempts to create / update the revisions table. This is separated since Ent attempts to wrap the migration
 // execution in a transaction and assumes the underlying connection is of type *sql.DB, which is not true for actually
 // reading and writing revisions.
-func (r *EntRevisions) Migrate(ctx context.Context) error {
+func (r *EntRevisions) Migrate(ctx context.Context) (err error) {
 	c := ent.NewClient(ent.Driver(sql.OpenDB(r.ac.Name, r.ac.DB)))
 	// Ensure the ent client is bound to the requested revision schema. Open a new connection, if not.
 	if r.ac.Name != dialect.SQLite && r.ac.URL.Schema != r.schema {
@@ -133,6 +134,30 @@ func (r *EntRevisions) Migrate(ctx context.Context) error {
 		defer sc.Close()
 		c = ent.NewClient(ent.Driver(sql.OpenDB(sc.Name, sc.DB)))
 	}
+	if r.ac.Name == dialect.SQLite {
+		var on sql.NullBool
+		if err := r.ac.DB.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&on); err != nil {
+			return err
+		}
+		if !on.Bool {
+			// Ent requires the foreign key checks in SQLite to be enabled for migration. Since Atlas does not,
+			// ensure they are set for the migration attempt and restore previous setting afterwards.
+			_, err := r.ac.ExecContext(ctx, "PRAGMA foreign_keys = on")
+			if err != nil {
+				return err
+			}
+			defer func() {
+				_, err2 := r.ac.ExecContext(ctx, "PRAGMA foreign_keys = off")
+				if err2 != nil {
+					if err != nil {
+						err = fmt.Errorf("%v: %w", err2, err)
+						return
+					}
+					err = err2
+				}
+			}()
+		}
+	}
 	return c.Schema.Create(ctx, entschema.WithDropColumn(true))
 }
 
diff --git a/cmd/atlas/internal/migrate/report.go b/cmd/atlas/internal/migrate/report.go
new file mode 100644
index 00000000..c91f55eb
--- /dev/null
+++ b/cmd/atlas/internal/migrate/report.go
@@ -0,0 +1,289 @@
+// 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 migrate
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+	"text/template"
+
+	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlclient"
+	"github.com/fatih/color"
+	"github.com/olekukonko/tablewriter"
+)
+
+// Reporter is used to gather information about migration status.
+type Reporter struct {
+	// Client configures the connection to the database to file a StatusReport for.
+	Client *sqlclient.Client
+	// Dir is used for scanning and validating the migration directory.
+	Dir migrate.Dir
+	// ReportWriter writes the summary report.
+	ReportWriter ReportWriter
+	// Schema name the revision table resides in.
+	Schema string
+}
+
+// Status creates and writes a StatusReport.
+func (r *Reporter) Status(ctx context.Context) error {
+	rep, err := NewStatusReport(r.Client, r.Dir)
+	if err != nil {
+		return err
+	}
+	// Check if there already is a revision table in the defined schema.
+	// Inspect schema and check if the table does already exist.
+	sch, err := r.Client.InspectSchema(ctx, r.Schema, &schema.InspectOptions{Tables: []string{revision.Table}})
+	if err != nil && !schema.IsNotExistError(err) {
+		return err
+	}
+	if schema.IsNotExistError(err) || func() bool { _, ok := sch.Table(revision.Table); return !ok }() {
+		// Either schema or table does not exist.
+		rep.Pending = rep.Available
+	} else {
+		// Both exist, fetch their data.
+		rrw, err := NewEntRevisions(ctx, r.Client, WithSchema(r.Schema))
+		if err != nil {
+			return err
+		}
+		if err := rrw.Migrate(ctx); err != nil {
+			return err
+		}
+		ex, err := migrate.NewExecutor(r.Client.Driver, r.Dir, rrw)
+		if err != nil {
+			return err
+		}
+		rep.Pending, err = ex.Pending(ctx)
+		if err != nil && !errors.Is(err, migrate.ErrNoPendingFiles) {
+			return err
+		}
+		rep.Applied, err = rrw.ReadRevisions(ctx)
+		if err != nil {
+			return err
+		}
+	}
+	switch len(rep.Pending) {
+	case len(rep.Available):
+		rep.Current = "No migration applied yet"
+	default:
+		rep.Current = rep.Applied[len(rep.Applied)-1].Version
+	}
+	if len(rep.Pending) == 0 {
+		rep.Status = "OK"
+		rep.Next = "Already at latest version"
+	} else {
+		rep.Status = "PENDING"
+		rep.Next = rep.Pending[0].Version()
+	}
+	// If the last one is partially applied (and not manually resolved).
+	if len(rep.Applied) != 0 {
+		last := rep.Applied[len(rep.Applied)-1]
+		if !last.Type.Has(migrate.RevisionTypeResolved) && last.Applied < last.Total {
+			rep.SQL = strings.ReplaceAll(last.ErrorStmt, "\n", " ")
+			rep.Error = strings.ReplaceAll(last.Error, "\n", " ")
+			rep.Count = last.Applied
+			idx := migrate.FilesLastIndex(rep.Available, func(f migrate.File) bool {
+				return f.Version() == last.Version
+			})
+			if idx == -1 {
+				return fmt.Errorf("migration file with version %q not found", last.Version)
+			}
+			stmts, err := rep.Available[idx].Stmts()
+			if err != nil {
+				return err
+			}
+			rep.Total = len(stmts)
+		}
+	}
+	return r.ReportWriter.WriteReport(rep)
+}
+
+var (
+	// TemplateFuncs are global functions available in templates.
+	TemplateFuncs = template.FuncMap{
+		"json": func(v any, args ...string) (string, error) {
+			var (
+				b   []byte
+				err error
+			)
+			switch len(args) {
+			case 0:
+				b, err = json.Marshal(v)
+			case 1:
+				b, err = json.MarshalIndent(v, "", args[0])
+			default:
+				b, err = json.MarshalIndent(v, args[0], args[1])
+			}
+			return string(b), err
+		},
+		"table":  table,
+		"cyan":   color.CyanString,
+		"green":  color.HiGreenString,
+		"red":    color.HiRedString,
+		"yellow": color.YellowString,
+		"default": func(report *StatusReport) (string, error) {
+			var buf bytes.Buffer
+			t, err := template.New("report").Funcs(template.FuncMap{
+				"cyan":   color.CyanString,
+				"green":  color.HiGreenString,
+				"red":    color.HiRedString,
+				"yellow": color.YellowString,
+			}).Parse(`Migration Status:
+{{- if eq .Status "OK"      }} {{ green .Status }}{{ end }}
+{{- if eq .Status "PENDING" }} {{ yellow .Status }}{{ end }}
+  {{ yellow "--" }} Current Version: {{ cyan .Current }}
+{{- if gt .Total 0 }}{{ printf " (%s statements applied)" (yellow "%d" .Count) }}{{ end }}
+  {{ yellow "--" }} Next Version:    {{ cyan .Next }}
+{{- if gt .Total 0 }}{{ printf " (%s statements left)" (yellow "%d" .Left) }}{{ end }}
+  {{ yellow "--" }} Executed Files:  {{ len .Applied }}{{ if gt .Total 0 }} (last one partially){{ end }}
+  {{ yellow "--" }} Pending Files:   {{ len .Pending }}
+{{ if gt .Total 0 }}
+Last migration attempt had errors:
+  {{ yellow "--" }} SQL:   {{ .SQL }}
+  {{ yellow "--" }} {{ red "ERROR:" }} {{ .Error }}
+{{ end }}`)
+			if err != nil {
+				return "", err
+			}
+			err = t.Execute(&buf, report)
+			return buf.String(), err
+		},
+	}
+	DefaultTemplate = template.Must(template.New("report").Funcs(TemplateFuncs).Parse("{{ default . }}"))
+)
+
+type (
+	// Env holds the environment information.
+	Env struct {
+		Driver string         `json:"Driver,omitempty"` // Driver name.
+		URL    *sqlclient.URL `json:"URL,omitempty"`    // URL to dev database.
+		Dir    string         `json:"Dir,omitempty"`    // Path to migration directory.
+	}
+
+	// StatusReport contains a summary of the migration status of a database.
+	StatusReport struct {
+		Env           `json:"Env"`
+		Available     Files               `json:"Available,omitempty"` // Available migration files.
+		Pending       Files               `json:"Pending,omitempty"`   // Pending migration files.
+		Applied       []*migrate.Revision `json:"Applied,omitempty"`   // Applied migration files.
+		Current, Next string              // Current and Next migration version.
+		Count, Total  int                 // Count of Total statements applied of the last revision.
+		Status        string              // Status of migration (OK, PENDING).
+		Error         string              `json:"Error,omitempty"` // Last Error that occurred.
+		SQL           string              `json:"SQL,omitempty"`   // SQL that caused the last Error.
+	}
+
+	// ReportWriter writes a StatusReport.
+	ReportWriter interface {
+		WriteReport(*StatusReport) error
+	}
+
+	// A TemplateWriter is ReportWrite that writes output according to a template.
+	TemplateWriter struct {
+		T *template.Template
+		W io.Writer
+	}
+
+	// Files is a slice of migrate.File. Implements json.Marshaler.
+	Files []migrate.File
+)
+
+// NewStatusReport returns a new StatusReport.
+func NewStatusReport(c *sqlclient.Client, dir migrate.Dir) (*StatusReport, error) {
+	files, err := dir.Files()
+	if err != nil {
+		return nil, err
+	}
+	sum := &StatusReport{
+		Env: Env{
+			Driver: c.Name,
+			URL:    c.URL,
+		},
+		Available: files,
+	}
+	if p, ok := dir.(interface{ Path() string }); ok {
+		sum.Env.Dir = p.Path()
+	}
+	return sum, nil
+}
+
+// WriteReport implements ReportWriter.
+func (w *TemplateWriter) WriteReport(r *StatusReport) error {
+	return w.T.Execute(w.W, r)
+}
+
+// MarshalJSON implements json.Marshaler.
+func (fs Files) MarshalJSON() ([]byte, error) {
+	type file struct {
+		Name        string `json:"Name,omitempty"`
+		Version     string `json:"Version,omitempty"`
+		Description string `json:"Description,omitempty"`
+	}
+	files := make([]file, len(fs))
+	for i, f := range fs {
+		files[i] = file{f.Name(), f.Version(), f.Desc()}
+	}
+	return json.Marshal(files)
+}
+
+// Left returns the amount of statements left to apply (if any).
+func (r *StatusReport) Left() int { return r.Total - r.Count }
+
+func table(report *StatusReport) (string, error) {
+	var buf strings.Builder
+	tbl := tablewriter.NewWriter(&buf)
+	tbl.SetRowLine(true)
+	tbl.SetAutoMergeCellsByColumnIndex([]int{0})
+	tbl.SetHeader([]string{
+		"Version",
+		"Description",
+		"Status",
+		"Count",
+		"Executed At",
+		"Execution Time",
+		"Error",
+		"SQL",
+	})
+	for _, r := range report.Applied {
+		tbl.Append([]string{
+			r.Version,
+			r.Description,
+			r.Type.String(),
+			fmt.Sprintf("%d/%d", r.Applied, r.Total),
+			r.ExecutedAt.Format("2006-01-02 15:04:05 MST"),
+			r.ExecutionTime.String(),
+			r.Error,
+			r.ErrorStmt,
+		})
+	}
+	for i, f := range report.Pending {
+		var c string
+		if i == 0 {
+			if r := report.Applied[len(report.Applied)-1]; f.Version() == r.Version && r.Applied < r.Total {
+				stmts, err := f.Stmts()
+				if err != nil {
+					return "", err
+				}
+				c = fmt.Sprintf("%d/%d", len(stmts)-r.Applied, len(stmts))
+			}
+		}
+		tbl.Append([]string{
+			f.Version(),
+			f.Desc(),
+			"pending",
+			c,
+			"", "", "", "",
+		})
+	}
+	tbl.Render()
+	return buf.String(), nil
+}
diff --git a/doc/md/reference.md b/doc/md/reference.md
index d9df5c7f..3a100ff4 100644
--- a/doc/md/reference.md
+++ b/doc/md/reference.md
@@ -281,6 +281,7 @@ atlas migrate status [flags]
 ```
 #### Flags
 ```
+      --log string                custom logging using a Go template
       --revisions-schema string   schema name where the revisions table resides
   -u, --url string                [driver://username:password@address/dbname?param=value] select a database using the URL format
 
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index 0bafe563..2ca0084f 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -20,7 +20,7 @@ require (
 )
 
 require (
-	ariga.io/atlas/cmd/atlas v0.7.1-0.20220925103247-db83ea0cd178 // indirect
+	ariga.io/atlas/cmd/atlas v0.7.2 // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
@@ -50,8 +50,10 @@ require (
 	github.com/manifoldco/promptui v0.9.0 // indirect
 	github.com/mattn/go-colorable v0.1.9 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-runewidth v0.0.9 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
 	github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
 	github.com/pingcap/tidb/parser v0.0.0-20220817134052-9709249e523a // indirect
@@ -74,7 +76,6 @@ require (
 	google.golang.org/grpc v1.33.1 // indirect
 	google.golang.org/protobuf v1.25.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
-	gopkg.in/errgo.v2 v2.1.0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index 19ed5e16..e1e7819e 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -190,6 +190,8 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
 github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
@@ -209,6 +211,8 @@ github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
@@ -236,8 +240,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.8.2-0.20220112175052-f3cb5c2c6412 h1:BVgfta5l+NeAx6l597tEbF2TkiVi+Mw+2vxJjwSfyRU=
-github.com/rogpeppe/go-internal v1.8.2-0.20220112175052-f3cb5c2c6412/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -433,8 +435,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index c923aca1..93850629 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -178,31 +178,18 @@ type (
 
 	// A Revision denotes an applied migration in a deployment. Used to track migration executions state of a database.
 	Revision struct {
-		// Version of the migration.
-		Version string
-		// Description of this migration.
-		Description string
-		// Type of the migration.
-		Type RevisionType
-		// Applied denotes the amount of successfully applied statements of the revision.
-		Applied int
-		// Total denotes the total amount of statements of the migration.
-		Total int
-		// ExecutedAt denotes when this migration was started to be executed.
-		ExecutedAt time.Time
-		// ExecutionTime denotes the time it took for this migration to be applied on the database.
-		ExecutionTime time.Duration
-		// Error holds information about a migration error (if occurred).
-		// If the error is from the application level, it is prefixed with "Go:\n".
-		// If the error is raised from the database, Error contains both the failed statement and the database error
-		// following the "SQL:\n<sql>\n\nError:\n<err>" format.
-		Error string
-		// Hash is the check-sum of this migration as stated by the migration directories HashFile.
-		Hash string
-		// PartialHashes contains one hash per statement that has been applied on the database.
-		PartialHashes []string
-		// OperatorVersion holds a string representation of the Atlas operator managing this database migration.
-		OperatorVersion string
+		Version         string        `json:"Version"`             // Version of the migration.
+		Description     string        `json:"Description"`         // Description of this migration.
+		Type            RevisionType  `json:"Type"`                // Type of the migration.
+		Applied         int           `json:"Applied"`             // Applied amount of statements in the migration.
+		Total           int           `json:"Total"`               // Total amount of statements in the migration.
+		ExecutedAt      time.Time     `json:"ExecutedAt"`          // ExecutedAt is the starting point of execution.
+		ExecutionTime   time.Duration `json:"ExecutionTime"`       // ExecutionTime of the migration.
+		Error           string        `json:"Error,omitempty"`     // Error of the migration, if any occurred.
+		ErrorStmt       string        `json:"ErrorStmt,omitempty"` // ErrorStmt is the statement that raised Error.
+		Hash            string        `json:"-"`                   // Hash of migration file.
+		PartialHashes   []string      `json:"-"`                   // PartialHashes is the hashes of applied statements.
+		OperatorVersion string        `json:"OperatorVersion"`     // OperatorVersion that executed this migration.
 	}
 
 	// RevisionType defines the type of the revision record in the history table.
@@ -244,6 +231,36 @@ const (
 	RevisionTypeResolved
 )
 
+// Has returns if the given flag is set.
+func (r RevisionType) Has(f RevisionType) bool {
+	return r&f != 0
+}
+
+// String implements fmt.Stringer.
+func (r RevisionType) String() string {
+	switch {
+	case r == RevisionTypeBaseline:
+		return "baseline"
+	case r == RevisionTypeExecute:
+		return "applied"
+	case r == RevisionTypeResolved:
+		return "manually set"
+	case r == RevisionTypeExecute|RevisionTypeResolved:
+		return "applied + manually set"
+	default:
+		return "unknown"
+	}
+}
+
+// MarshalText implements encoding.TextMarshaler.
+func (r RevisionType) MarshalText() ([]byte, error) {
+	s := r.String()
+	if s == "unknown" {
+		return nil, fmt.Errorf("unexpected revision type '%v'", r)
+	}
+	return []byte(s), nil
+}
+
 // NewPlanner creates a new Planner.
 func NewPlanner(drv Driver, dir Dir, opts ...PlannerOption) *Planner {
 	p := &Planner{drv: drv, dir: dir, sum: true}
@@ -640,7 +657,9 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 		e.log.Log(LogStmt{stmt})
 		if _, err = e.drv.ExecContext(ctx, stmt); err != nil {
 			e.log.Log(LogError{Error: err})
-			r.setSQLErr(stmt, err)
+			r.done()
+			r.ErrorStmt = stmt
+			r.Error = err.Error()
 			return fmt.Errorf("sql/migrate: execute: executing statement %q from version %q: %w", stmt, r.Version, err)
 		}
 		r.PartialHashes = append(r.PartialHashes, "h1:"+sums[r.Applied])
@@ -791,11 +810,6 @@ func (r *Revision) done() {
 	r.ExecutionTime = time.Now().Sub(r.ExecutedAt)
 }
 
-func (r *Revision) setSQLErr(stmt string, err error) {
-	r.done()
-	r.Error = fmt.Sprintf("Statement:\n%s\n\nError:\n%s", stmt, err)
-}
-
 type (
 	// A Logger logs migration execution.
 	Logger interface {


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index 59b9add5..3baa2f99 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -503,7 +503,8 @@ func TestMigrate_Diff(t *testing.T) {
 
 	// A lock will prevent diffing.
 	sqlclient.Register("sqlitelockdiff", sqlclient.OpenerFunc(func(ctx context.Context, u *url.URL) (*sqlclient.Client, error) {
-		client, err := sqlclient.Open(ctx, strings.Replace(u.String(), u.Scheme, "sqlite", 1))
+		u.Scheme = "sqlite"
+		client, err := sqlclient.OpenURL(ctx, u)
 		if err != nil {
 			return nil, err
 		}
diff --git a/cmd/atlas/internal/migrate/migrate_test.go b/cmd/atlas/internal/migrate/migrate_test.go
index 68a7a668..6b00e44a 100644
--- a/cmd/atlas/internal/migrate/migrate_test.go
+++ b/cmd/atlas/internal/migrate/migrate_test.go
@@ -6,8 +6,6 @@ package migrate
 
 import (
 	"context"
-	"fmt"
-	"path/filepath"
 	"testing"
 
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
@@ -17,10 +15,7 @@ import (
 )
 
 func TestNewEntRevisions(t *testing.T) {
-	c, err := sqlclient.Open(
-		context.Background(),
-		fmt.Sprintf("sqlite://%s?cache=shared&mode=memory&_fk=true", filepath.Join(t.TempDir(), "revision")),
-	)
+	c, err := sqlclient.Open(context.Background(), "sqlite://?mode=memory")
 	require.NoError(t, err)
 	r, err := NewEntRevisions(context.Background(), c)
 	require.NoError(t, err)
diff --git a/cmd/atlas/internal/migrate/report_test.go b/cmd/atlas/internal/migrate/report_test.go
new file mode 100644
index 00000000..523e6a40
--- /dev/null
+++ b/cmd/atlas/internal/migrate/report_test.go
@@ -0,0 +1,116 @@
+// 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 migrate
+
+import (
+	"context"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/sqlclient"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReporter_Status(t *testing.T) {
+	var (
+		buf strings.Builder
+		ctx = context.Background()
+	)
+
+	// Clean.
+	dir, err := migrate.NewLocalDir(filepath.Join("testdata", "broken"))
+	require.NoError(t, err)
+	c, err := sqlclient.Open(ctx, "sqlite://?mode=memory")
+	require.NoError(t, err)
+	defer c.Close()
+	require.NoError(t, (&Reporter{
+		Client:       c,
+		Dir:          dir,
+		ReportWriter: &TemplateWriter{T: DefaultTemplate, W: &buf},
+	}).Status(ctx))
+	require.Equal(t, `Migration Status: PENDING
+  -- Current Version: No migration applied yet
+  -- Next Version:    1
+  -- Executed Files:  0
+  -- Pending Files:   3
+`, buf.String())
+
+	// Applied one.
+	buf.Reset()
+	rrw, err := NewEntRevisions(ctx, c)
+	require.NoError(t, err)
+	require.NoError(t, rrw.Migrate(ctx))
+	ex, err := migrate.NewExecutor(c.Driver, dir, rrw)
+	require.NoError(t, err)
+	require.NoError(t, ex.ExecuteN(ctx, 1))
+	require.NoError(t, (&Reporter{
+		Client:       c,
+		Dir:          dir,
+		ReportWriter: &TemplateWriter{T: DefaultTemplate, W: &buf},
+	}).Status(ctx))
+	require.Equal(t, `Migration Status: PENDING
+  -- Current Version: 1
+  -- Next Version:    2
+  -- Executed Files:  1
+  -- Pending Files:   2
+`, buf.String())
+
+	// Applied two.
+	buf.Reset()
+	require.NoError(t, err)
+	require.NoError(t, ex.ExecuteN(ctx, 1))
+	require.NoError(t, (&Reporter{
+		Client:       c,
+		Dir:          dir,
+		ReportWriter: &TemplateWriter{T: DefaultTemplate, W: &buf},
+	}).Status(ctx))
+	require.Equal(t, `Migration Status: PENDING
+  -- Current Version: 2
+  -- Next Version:    3
+  -- Executed Files:  2
+  -- Pending Files:   1
+`, buf.String())
+
+	// Partial three.
+	buf.Reset()
+	require.NoError(t, err)
+	require.Error(t, ex.ExecuteN(ctx, 1))
+	require.NoError(t, (&Reporter{
+		Client:       c,
+		Dir:          dir,
+		ReportWriter: &TemplateWriter{T: DefaultTemplate, W: &buf},
+	}).Status(ctx))
+	require.Equal(t, `Migration Status: PENDING
+  -- Current Version: 3 (1 statements applied)
+  -- Next Version:    3 (1 statements left)
+  -- Executed Files:  3 (last one partially)
+  -- Pending Files:   1
+
+Last migration attempt had errors:
+  -- SQL:   THIS LINE ADDS A SYNTAX ERROR;
+  -- ERROR: near "THIS": syntax error
+`, buf.String())
+
+	// Fixed three - okay.
+	buf.Reset()
+	dir2, err := migrate.NewLocalDir(filepath.Join("testdata", "fixed"))
+	require.NoError(t, err)
+	*dir = *dir2
+	require.NoError(t, err)
+	require.NoError(t, ex.ExecuteN(ctx, 1))
+	require.NoError(t, (&Reporter{
+		Client:       c,
+		Dir:          dir,
+		ReportWriter: &TemplateWriter{T: DefaultTemplate, W: &buf},
+	}).Status(ctx))
+	require.Equal(t, `Migration Status: OK
+  -- Current Version: 3
+  -- Next Version:    Already at latest version
+  -- Executed Files:  3
+  -- Pending Files:   0
+`, buf.String())
+}
diff --git a/cmd/atlas/internal/migrate/testdata/broken/1.sql b/cmd/atlas/internal/migrate/testdata/broken/1.sql
new file mode 100644
index 00000000..e48fb97b
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/broken/1.sql
@@ -0,0 +1,1 @@
+CREATE TABLE `users` (`id` int NOT NULL PRIMARY KEY);
\ No newline at end of file
diff --git a/cmd/atlas/internal/migrate/testdata/broken/2.sql b/cmd/atlas/internal/migrate/testdata/broken/2.sql
new file mode 100644
index 00000000..048e128c
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/broken/2.sql
@@ -0,0 +1,1 @@
+ALTER TABLE `users` ADD COLUMN `happy` boolean NOT NULL DEFAULT true;
\ No newline at end of file
diff --git a/cmd/atlas/internal/migrate/testdata/broken/3.sql b/cmd/atlas/internal/migrate/testdata/broken/3.sql
new file mode 100644
index 00000000..90be5e62
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/broken/3.sql
@@ -0,0 +1,2 @@
+CREATE TABLE `pets` (`id` int NOT NULL PRIMARY KEY);
+THIS LINE ADDS A SYNTAX ERROR;
\ No newline at end of file
diff --git a/cmd/atlas/internal/migrate/testdata/broken/atlas.sum b/cmd/atlas/internal/migrate/testdata/broken/atlas.sum
new file mode 100644
index 00000000..80d98ff5
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/broken/atlas.sum
@@ -0,0 +1,4 @@
+h1:0YTkvowN+aAuYuJY5ZANPAq6QAZ0wAXunU9sUXsuZcI=
+1.sql h1:twN+zPVp8JzWEUcPPfNIT6/62Wa08dfxxZhT4gZxBzg=
+2.sql h1:KHLlrSPwSjbp4+KrpQHwYZf8zlSIiAR7MS3H2yd1yeE=
+3.sql h1:iTKab5MEzWsTdVVDkCUWmateRs9XRNpal/cxqlCYsmQ=
diff --git a/cmd/atlas/internal/migrate/testdata/fixed/1.sql b/cmd/atlas/internal/migrate/testdata/fixed/1.sql
new file mode 100644
index 00000000..e48fb97b
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/fixed/1.sql
@@ -0,0 +1,1 @@
+CREATE TABLE `users` (`id` int NOT NULL PRIMARY KEY);
\ No newline at end of file
diff --git a/cmd/atlas/internal/migrate/testdata/fixed/2.sql b/cmd/atlas/internal/migrate/testdata/fixed/2.sql
new file mode 100644
index 00000000..048e128c
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/fixed/2.sql
@@ -0,0 +1,1 @@
+ALTER TABLE `users` ADD COLUMN `happy` boolean NOT NULL DEFAULT true;
\ No newline at end of file
diff --git a/cmd/atlas/internal/migrate/testdata/fixed/3.sql b/cmd/atlas/internal/migrate/testdata/fixed/3.sql
new file mode 100644
index 00000000..90f7827a
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/fixed/3.sql
@@ -0,0 +1,2 @@
+CREATE TABLE `pets` (`id` int NOT NULL PRIMARY KEY);
+ALTER TABLE `pets` ADD COLUMN `happy` boolean NULL;
\ No newline at end of file
diff --git a/cmd/atlas/internal/migrate/testdata/fixed/atlas.sum b/cmd/atlas/internal/migrate/testdata/fixed/atlas.sum
new file mode 100644
index 00000000..2728d98b
--- /dev/null
+++ b/cmd/atlas/internal/migrate/testdata/fixed/atlas.sum
@@ -0,0 +1,4 @@
+h1:oNV0f9HzMMCf2pzF+sA6CmsSHGjdkWYmcTjoc/lWxLs=
+1.sql h1:twN+zPVp8JzWEUcPPfNIT6/62Wa08dfxxZhT4gZxBzg=
+2.sql h1:KHLlrSPwSjbp4+KrpQHwYZf8zlSIiAR7MS3H2yd1yeE=
+3.sql h1:osHrPgu8uNgd6j09XCiyBQWWEh9aAmwjRgzdZZgm68M=
diff --git a/internal/integration/testdata/postgres/cli-migrate-status.txt b/internal/integration/testdata/postgres/cli-migrate-status.txt
index d2c3802f..eba18988 100644
--- a/internal/integration/testdata/postgres/cli-migrate-status.txt
+++ b/internal/integration/testdata/postgres/cli-migrate-status.txt
@@ -31,19 +31,19 @@ ALTER TABLE "users" ADD COLUMN "happy" boolean NOT NULL DEFAULT true;
 
 -- status_clean.txt --
 Migration Status: PENDING
-  -- Current Version:  No version applied yet
-  -- Next Version:     1
-  -- Executed Files:   0
-  -- Pending Files:    2
+  -- Current Version: No migration applied yet
+  -- Next Version:    1
+  -- Executed Files:  0
+  -- Pending Files:   2
 -- status_one_applied.txt --
 Migration Status: PENDING
-  -- Current Version:  1
-  -- Next Version:     2
-  -- Executed Files:   1
-  -- Pending Files:    1
+  -- Current Version: 1
+  -- Next Version:    2
+  -- Executed Files:  1
+  -- Pending Files:   1
 -- status_ok.txt --
 Migration Status: OK
-  -- Current Version:  2
-  -- Next Version:     Already at latest version
-  -- Executed Files:   2
-  -- Pending Files:    0
\ No newline at end of file
+  -- Current Version: 2
+  -- Next Version:    Already at latest version
+  -- Executed Files:  2
+  -- Pending Files:   0
\ No newline at end of file
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index 8514bcae..16f7f9a1 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -21,6 +21,31 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestRevisionType_MarshalText(t *testing.T) {
+	for _, tt := range []struct {
+		r       migrate.RevisionType
+		ex      string
+		wantErr bool
+	}{
+		{migrate.RevisionTypeUnknown, "", true},
+		{migrate.RevisionTypeBaseline, "baseline", false},
+		{migrate.RevisionTypeExecute, "applied", false},
+		{migrate.RevisionTypeResolved, "manually set", false},
+		{migrate.RevisionTypeExecute | migrate.RevisionTypeResolved, "applied + manually set", false},
+		{migrate.RevisionTypeExecute | migrate.RevisionTypeBaseline, "", true},
+		{1 << 3, "", true},
+	} {
+		ac, err := tt.r.MarshalText()
+		if tt.wantErr {
+			require.Error(t, err)
+			require.Zero(t, ac)
+			continue
+		}
+		require.NoError(t, err)
+		require.Equal(t, tt.ex, string(ac))
+	}
+}
+
 func TestPlanner_WritePlan(t *testing.T) {
 	p := t.TempDir()
 	d, err := migrate.NewLocalDir(p)
@@ -367,7 +392,8 @@ func TestExecutor(t *testing.T) {
 		Type:            migrate.RevisionTypeExecute,
 		Applied:         1,
 		Total:           2,
-		Error:           "Statement:\nALTER TABLE t_sub ADD c4 int;\n\nError:\nthis is an error",
+		Error:           "this is an error",
+		ErrorStmt:       "ALTER TABLE t_sub ADD c4 int;",
 		OperatorVersion: "op",
 	}, revs[len(revs)-1])