Repository: ariga/atlas
Pull request #939 (2022-07-13 10:04:19)

sql/migrate: change execution logic
Instead of having a state for a migration execution like `ok`, `error`, ... we now have a counter for every executed statement. This will increase for every successfully applied statement. Every next attempt to migrate, will pick up the file where it left off and attempt to execute the new statement.

File has error: 
```
ALTER TABLE `tbl` ADD `col_3` bigint;
aALTER T sABLE `tbl` ADD `col4` bigint;
```
![image](https://user-images.githubusercontent.com/12862103/178929499-072fdd43-1bbf-4e9f-979d-bda6d8863839.png)

After fixing file:
```
ALTER TABLE `tbl` ADD `col_3` bigint;
ALTER TABLE `tbl` ADD `col4` bigint;
```
![image](https://user-images.githubusercontent.com/12862103/178929631-54b95044-e40e-4922-81a9-b7ac58e4731d.png)

Another attempt:
![image](https://user-images.githubusercontent.com/12862103/178929836-1a31e272-db37-4b55-8186-1438e66fbaa8.png)



Patch:
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index 603ac066..cefdc808 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -3,18 +3,18 @@ module ariga.io/atlas/cmd/atlas
 go 1.18
 
 require (
-	ariga.io/atlas v0.4.3-0.20220713104915-cda1480dd791
-	entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0
+	ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7
+	entgo.io/ent v0.11.1
 	github.com/fatih/color v1.13.0
 	github.com/go-sql-driver/mysql v1.6.0
-	github.com/hashicorp/hcl/v2 v2.10.0
-	github.com/lib/pq v1.10.5
+	github.com/hashicorp/hcl/v2 v2.13.0
+	github.com/lib/pq v1.10.6
 	github.com/manifoldco/promptui v0.9.0
-	github.com/mattn/go-sqlite3 v1.14.10
+	github.com/mattn/go-sqlite3 v1.14.14
 	github.com/mitchellh/go-homedir v1.1.0
-	github.com/spf13/cobra v1.4.0
+	github.com/spf13/cobra v1.5.0
 	github.com/spf13/pflag v1.0.5
-	github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
+	github.com/stretchr/testify v1.8.0
 	golang.org/x/mod v0.5.1
 )
 
@@ -27,12 +27,14 @@ require (
 	github.com/google/go-cmp v0.5.6 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
-	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/mattn/go-colorable v0.1.9 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/zclconf/go-cty v1.8.0 // indirect
 	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
 	golang.org/x/text v0.3.7 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index a6fbc56d..c283642e 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -1,14 +1,10 @@
-ariga.io/atlas v0.4.3-0.20220711075326-c3fb0f64fd0e h1:BFNDhM1I7hZrgS1k2UOUm1L9btGzB4L3dHsgDsTad7Q=
-ariga.io/atlas v0.4.3-0.20220711075326-c3fb0f64fd0e/go.mod h1:ofVetkJqlaWle3mvYmaS2uyFGFcc7dSq436tmxa/Mzk=
-ariga.io/atlas v0.4.3-0.20220713104915-cda1480dd791 h1:gzAeuWIe04MS2c1C/1RHAqvk78wKDR2vaK78EscMBgo=
-ariga.io/atlas v0.4.3-0.20220713104915-cda1480dd791/go.mod h1:ofVetkJqlaWle3mvYmaS2uyFGFcc7dSq436tmxa/Mzk=
-entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0 h1:qHA4+ANAzDj6BcDLxNgZuzKxFre/RI9r5wwsI2O+1M4=
-entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0/go.mod h1:Zh61BPvB+cL6VWEyN8f1YoDacrMjQf2KDlDeX26xq2k=
+ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7 h1:SSmcPvU2K/1U2D5S12xIbriQ5FUj88cuW80H4aGNuOQ=
+ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7/go.mod h1:ofVetkJqlaWle3mvYmaS2uyFGFcc7dSq436tmxa/Mzk=
+entgo.io/ent v0.11.1 h1:im67R+2W3Nee2bNS2YnoYz8oAF0Qz4AOlIvKRIAEISY=
+entgo.io/ent v0.11.1/go.mod h1:X5b1YfMayrRTgKGO//8IqpL7XJx0uqdeReEkxNpXROA=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
 github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
-github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
-github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
 github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
@@ -17,7 +13,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 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/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -28,8 +25,6 @@ github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12f
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
-github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
-github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -37,8 +32,8 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg=
-github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
+github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
+github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -47,61 +42,49 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
-github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
-github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
-github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
+github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
+github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 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-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
-github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
-github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
-github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
+github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
-github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
 github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
-github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
 github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
 github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
-github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210927094055-39ccf1dd6fa6/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -110,14 +93,17 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 h1:M1gcVrIb2lSn2FIL19DG0+/b8nNVKJ7W7b4WcAGZAYM=
+golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/cmd/atlas/go.work.sum b/cmd/atlas/go.work.sum
index 56f9d847..488a66df 100644
--- a/cmd/atlas/go.work.sum
+++ b/cmd/atlas/go.work.sum
@@ -1,1 +1,2 @@
 github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
+golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 h1:M1gcVrIb2lSn2FIL19DG0+/b8nNVKJ7W7b4WcAGZAYM=
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 1b441ecd..ebb697bc 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -297,6 +297,41 @@ func CmdMigrateApplyRun(cmd *cobra.Command, args []string) error {
 			}
 		}
 	}(rrw.(*entmigrate.EntRevisions), cmd.Context())
+	// Determine pending files and lock the database while working.
+	ex, err := migrate.NewExecutor(c.Driver, dir, rrw, migrate.WithLogger(l))
+	if err != nil {
+		return err
+	}
+	unlock, err := ex.Lock(cmd.Context())
+	if err != nil {
+		return err
+	}
+	defer unlock()
+	pending, err := ex.Pending(cmd.Context())
+	if err != nil && !errors.Is(err, migrate.ErrNoPendingFiles) {
+		return err
+	}
+	if errors.Is(err, migrate.ErrNoPendingFiles) {
+		cmd.Println("The migration directory is synced with the database, no migration files to execute")
+		return nil
+	}
+	if n > 0 {
+		// Cannot apply more than len(pending) files.
+		if n >= len(pending) {
+			n = len(pending)
+		}
+		pending = pending[:n]
+	}
+	revs, err := rrw.ReadRevisions(cmd.Context())
+	if err != nil {
+		return err
+	}
+	if err := migrate.LogIntro(struct {
+		migrate.Logger
+		migrate.Scanner
+	}{l, dir.(migrate.Scanner)}, revs, pending); err != nil {
+		return err
+	}
 	var (
 		drv migrate.Driver
 		tx  *sqlclient.TxClient
@@ -304,33 +339,35 @@ func CmdMigrateApplyRun(cmd *cobra.Command, args []string) error {
 	if MigrateFlags.DryRun {
 		drv = &dryRunDriver{c.Driver}
 		rrw = &dryRunRevisions{rrw}
-	} else {
-		// Wrap the whole execution in one transaction. This behaviour will change once
-		// there are insights about migration files available.
-		tx, err = c.Tx(cmd.Context(), nil)
+	}
+	for _, f := range pending {
+		if !MigrateFlags.DryRun {
+			// Wrap the file execution in a transaction.
+			tx, err = c.Tx(cmd.Context(), nil)
+			if err != nil {
+				return err
+			}
+			drv = tx.Driver
+		}
+		ex, err := migrate.NewExecutor(drv, dir, rrw, migrate.WithLogger(l))
 		if err != nil {
 			return err
 		}
-		drv = tx.Driver
-	}
-	// Get the executor.
-	ex, err := migrate.NewExecutor(drv, dir, rrw, migrate.WithLogger(l))
-	if err != nil {
-		return err
-	}
-	if err = ex.ExecuteN(cmd.Context(), n); errors.Is(err, migrate.ErrNoPendingFiles) {
-		cmd.Println("The migration directory is synced with the database, no migration files to execute")
-	} else if err != nil {
+		if err := ex.Execute(cmd.Context(), f); err != nil {
+			if !MigrateFlags.DryRun {
+				if err2 := tx.Rollback(); err2 != nil {
+					err = fmt.Errorf("%v: %w", err2, err)
+				}
+			}
+			return err
+		}
 		if !MigrateFlags.DryRun {
-			if err2 := tx.Rollback(); err2 != nil {
-				err = fmt.Errorf("%v: %w", err2, err)
+			if err := tx.Commit(); err != nil {
+				return err
 			}
 		}
-		return err
-	}
-	if !MigrateFlags.DryRun {
-		return tx.Commit()
 	}
+	l.Log(migrate.LogDone{})
 	return nil
 }
 
@@ -454,35 +491,13 @@ func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
 	if err != nil {
 		return err
 	}
-	dirty := migrate.DirtyError{}
-	pending, err := ex.Pending(context.Background())
-	if err != nil && !errors.As(err, &dirty) && !errors.Is(err, migrate.ErrNoPendingFiles) {
+	pending, err := ex.Pending(cmd.Context())
+	if err != nil && !errors.Is(err, migrate.ErrNoPendingFiles) {
 		return err
 	}
-	if errors.As(err, &dirty) {
-		var c func(string, ...interface{}) string
-		switch dirty.State {
-		case migrate.StateOngoing:
-			c = yellow
-		case migrate.StateError:
-			c = red
-		}
-		s := c(strings.ToUpper(dirty.State))
-		msgPrint(cmd.OutOrStdout(), s, fmt.Sprintf("Dirty migration state: version %s has state %s", dirty.Version, s))
-		return nil
-	}
 	return statusPrint(cmd.OutOrStdout(), sc, avail, avail[:len(pending)+1], pending)
 }
 
-func headerPrint(out io.Writer, state string) {
-	fmt.Fprintf(out, "Migration Status: %s\n", state)
-}
-
-func msgPrint(out io.Writer, state, msg string) {
-	headerPrint(out, state)
-	fmt.Fprintln(out, msg)
-}
-
 func statusPrint(out io.Writer, sc migrate.Scanner, avail, applied, pending []migrate.File) (err error) {
 	var cur, next, state string
 	if len(applied) == 0 {
@@ -503,7 +518,7 @@ func statusPrint(out io.Writer, sc migrate.Scanner, avail, applied, pending []mi
 			return err
 		}
 	}
-	headerPrint(out, state)
+	fmt.Fprintf(out, "Migration Status: %s\n", state)
 	fmt.Fprintf(out, "%s%s Current Version:\t%s\n", indent2, dash, cyan(cur))
 	fmt.Fprintf(out, "%s%s Next Version:\t%s\n", indent2, dash, cyan(next))
 	fmt.Fprintf(out, "%s%s Available:\t\t%s\n", indent2, dash, cyan(strconv.Itoa(len(avail))))
@@ -845,14 +860,15 @@ type LogTTY struct {
 }
 
 var (
-	cyan    = color.CyanString
-	green   = color.HiGreenString
-	red     = color.HiRedString
-	yellow  = color.YellowString
-	dash    = yellow("--")
-	arr     = cyan("->")
-	indent2 = strings.Repeat(" ", 2)
-	indent4 = strings.Repeat(indent2, 2)
+	cyan         = color.CyanString
+	green        = color.HiGreenString
+	red          = color.HiRedString
+	redBgWhiteFg = color.New(color.FgHiWhite, color.BgHiRed).SprintFunc()
+	yellow       = color.YellowString
+	dash         = yellow("--")
+	arr          = cyan("->")
+	indent2      = "  "
+	indent4      = indent2 + indent2
 )
 
 // Log implements the migrate.Logger interface.
@@ -871,7 +887,11 @@ func (l *LogTTY) Log(e migrate.LogEntry) {
 			l.reportFileEnd()
 		}
 		l.fileStart = time.Now()
-		fmt.Fprintf(l.out, "\n%s%v migrating version %v\n", indent2, dash, cyan(e.Version))
+		fmt.Fprintf(l.out, "\n%s%v migrating version %v", indent2, dash, cyan(e.Version))
+		if e.Skip > 0 {
+			fmt.Fprintf(l.out, " (partially applied - skipping %s statements)", yellow("%d", e.Skip))
+		}
+		fmt.Fprint(l.out, "\n")
 	case migrate.LogStmt:
 		l.stmtCounter++
 		fmt.Fprintf(l.out, "%s%v %s\n", indent4, arr, e.SQL)
@@ -881,6 +901,13 @@ func (l *LogTTY) Log(e migrate.LogEntry) {
 		fmt.Fprintf(l.out, "%s%v %v\n", indent2, dash, time.Since(l.start))
 		fmt.Fprintf(l.out, "%s%v %v migrations\n", indent2, dash, l.fileCounter)
 		fmt.Fprintf(l.out, "%s%v %v sql statements\n", indent2, dash, l.stmtCounter)
+	case migrate.LogError:
+		fmt.Fprintf(l.out, "%s %s\n", indent4, redBgWhiteFg(e.Error.Error()))
+		fmt.Fprintf(l.out, "\n%s%v\n", indent2, cyan(strings.Repeat("-", 25)))
+		fmt.Fprintf(l.out, "%s%v %v\n", indent2, dash, time.Since(l.start))
+		fmt.Fprintf(l.out, "%s%v %v migrations ok (%s)\n", indent2, dash, l.fileCounter-1, red("1 with errors"))
+		fmt.Fprintf(l.out, "%s%v %v sql statements ok (%s)\n", indent2, dash, l.stmtCounter-1, red("1 with errors"))
+		fmt.Fprintf(l.out, "\n%s\n%v\n\n", red("Error: Execution had errors:"), redBgWhiteFg(e.Error.Error()))
 	default:
 		fmt.Fprintf(l.out, "%v", e)
 	}
diff --git a/cmd/atlas/internal/migrate/ent/client.go b/cmd/atlas/internal/migrate/ent/client.go
index decb85ee..ae264b05 100644
--- a/cmd/atlas/internal/migrate/ent/client.go
+++ b/cmd/atlas/internal/migrate/ent/client.go
@@ -142,7 +142,7 @@ func (c *RevisionClient) Use(hooks ...Hook) {
 	c.hooks.Revision = append(c.hooks.Revision, hooks...)
 }
 
-// Create returns a create builder for Revision.
+// Create returns a builder for creating a Revision entity.
 func (c *RevisionClient) Create() *RevisionCreate {
 	mutation := newRevisionMutation(c.config, OpCreate)
 	return &RevisionCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
@@ -177,12 +177,12 @@ func (c *RevisionClient) Delete() *RevisionDelete {
 	return &RevisionDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
 }
 
-// DeleteOne returns a delete builder for the given entity.
+// DeleteOne returns a builder for deleting the given entity.
 func (c *RevisionClient) DeleteOne(r *Revision) *RevisionDeleteOne {
 	return c.DeleteOneID(r.ID)
 }
 
-// DeleteOneID returns a delete builder for the given id.
+// DeleteOne returns a builder for deleting the given entity by its id.
 func (c *RevisionClient) DeleteOneID(id string) *RevisionDeleteOne {
 	builder := c.Delete().Where(revision.ID(id))
 	builder.mutation.id = &id
diff --git a/cmd/atlas/internal/migrate/ent/generate.go b/cmd/atlas/internal/migrate/ent/generate.go
index e9d8cf57..b78320d7 100644
--- a/cmd/atlas/internal/migrate/ent/generate.go
+++ b/cmd/atlas/internal/migrate/ent/generate.go
@@ -4,4 +4,4 @@
 
 package ent
 
-//go:generate go run -mod=mod entc.go
+//go:generate go run entc.go
diff --git a/cmd/atlas/internal/migrate/ent/migrate/migrate.go b/cmd/atlas/internal/migrate/ent/migrate/migrate.go
index f5dfb7fd..0e1a3042 100644
--- a/cmd/atlas/internal/migrate/ent/migrate/migrate.go
+++ b/cmd/atlas/internal/migrate/ent/migrate/migrate.go
@@ -32,9 +32,6 @@ var (
 	// and therefore, it's recommended to enable this option to get more
 	// flexibility in the schema changes.
 	WithDropIndex = schema.WithDropIndex
-	// WithFixture sets the foreign-key renaming option to the migration when upgrading
-	// ent from v0.1.0 (issue-#285). Defaults to false.
-	WithFixture = schema.WithFixture
 	// WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true.
 	WithForeignKeys = schema.WithForeignKeys
 )
@@ -49,11 +46,16 @@ func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} }
 
 // Create creates all schema resources.
 func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error {
+	return Create(ctx, s, Tables, opts...)
+}
+
+// Create creates all table resources using the given schema driver.
+func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...schema.MigrateOption) error {
 	migrate, err := schema.NewMigrate(s.drv, opts...)
 	if err != nil {
 		return fmt.Errorf("ent/migrate: %w", err)
 	}
-	return migrate.Create(ctx, Tables...)
+	return migrate.Create(ctx, tables...)
 }
 
 // WriteTo writes the schema changes to w instead of running them against the database.
@@ -63,13 +65,5 @@ func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error
 // 	}
 //
 func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error {
-	drv := &schema.WriteDriver{
-		Writer: w,
-		Driver: s.drv,
-	}
-	migrate, err := schema.NewMigrate(drv, opts...)
-	if err != nil {
-		return fmt.Errorf("ent/migrate: %w", err)
-	}
-	return migrate.Create(ctx, Tables...)
+	return Create(ctx, &Schema{drv: &schema.WriteDriver{Writer: w, Driver: s.drv}}, Tables, opts...)
 }
diff --git a/cmd/atlas/internal/migrate/ent/migrate/schema.go b/cmd/atlas/internal/migrate/ent/migrate/schema.go
index 3a5dbf65..a96a5f3f 100644
--- a/cmd/atlas/internal/migrate/ent/migrate/schema.go
+++ b/cmd/atlas/internal/migrate/ent/migrate/schema.go
@@ -17,9 +17,11 @@ var (
 	AtlasSchemaRevisionsColumns = []*schema.Column{
 		{Name: "version", Type: field.TypeString},
 		{Name: "description", Type: field.TypeString},
-		{Name: "execution_state", Type: field.TypeEnum, Enums: []string{"ongoing", "ok", "error"}},
+		{Name: "applied", Type: field.TypeInt},
+		{Name: "total", Type: field.TypeInt},
 		{Name: "executed_at", Type: field.TypeTime},
 		{Name: "execution_time", Type: field.TypeInt64},
+		{Name: "error", Type: field.TypeString, Nullable: true},
 		{Name: "hash", Type: field.TypeString},
 		{Name: "operator_version", Type: field.TypeString},
 		{Name: "meta", Type: field.TypeJSON},
diff --git a/cmd/atlas/internal/migrate/ent/mutation.go b/cmd/atlas/internal/migrate/ent/mutation.go
index cff27a52..275ae8e9 100644
--- a/cmd/atlas/internal/migrate/ent/mutation.go
+++ b/cmd/atlas/internal/migrate/ent/mutation.go
@@ -38,10 +38,14 @@ type RevisionMutation struct {
 	typ               string
 	id                *string
 	description       *string
-	execution_state   *revision.ExecutionState
+	applied           *int
+	addapplied        *int
+	total             *int
+	addtotal          *int
 	executed_at       *time.Time
 	execution_time    *time.Duration
 	addexecution_time *time.Duration
+	error             *string
 	hash              *string
 	operator_version  *string
 	meta              *map[string]string
@@ -191,40 +195,116 @@ func (m *RevisionMutation) ResetDescription() {
 	m.description = nil
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (m *RevisionMutation) SetExecutionState(rs revision.ExecutionState) {
-	m.execution_state = &rs
+// SetApplied sets the "applied" field.
+func (m *RevisionMutation) SetApplied(i int) {
+	m.applied = &i
+	m.addapplied = nil
 }
 
-// ExecutionState returns the value of the "execution_state" field in the mutation.
-func (m *RevisionMutation) ExecutionState() (r revision.ExecutionState, exists bool) {
-	v := m.execution_state
+// Applied returns the value of the "applied" field in the mutation.
+func (m *RevisionMutation) Applied() (r int, exists bool) {
+	v := m.applied
 	if v == nil {
 		return
 	}
 	return *v, true
 }
 
-// OldExecutionState returns the old "execution_state" field's value of the Revision entity.
+// OldApplied returns the old "applied" 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) OldExecutionState(ctx context.Context) (v revision.ExecutionState, err error) {
+func (m *RevisionMutation) OldApplied(ctx context.Context) (v int, err error) {
 	if !m.op.Is(OpUpdateOne) {
-		return v, errors.New("OldExecutionState is only allowed on UpdateOne operations")
+		return v, errors.New("OldApplied is only allowed on UpdateOne operations")
 	}
 	if m.id == nil || m.oldValue == nil {
-		return v, errors.New("OldExecutionState requires an ID field in the mutation")
+		return v, errors.New("OldApplied requires an ID field in the mutation")
 	}
 	oldValue, err := m.oldValue(ctx)
 	if err != nil {
-		return v, fmt.Errorf("querying old value for OldExecutionState: %w", err)
+		return v, fmt.Errorf("querying old value for OldApplied: %w", err)
 	}
-	return oldValue.ExecutionState, nil
+	return oldValue.Applied, nil
 }
 
-// ResetExecutionState resets all changes to the "execution_state" field.
-func (m *RevisionMutation) ResetExecutionState() {
-	m.execution_state = nil
+// AddApplied adds i to the "applied" field.
+func (m *RevisionMutation) AddApplied(i int) {
+	if m.addapplied != nil {
+		*m.addapplied += i
+	} else {
+		m.addapplied = &i
+	}
+}
+
+// AddedApplied returns the value that was added to the "applied" field in this mutation.
+func (m *RevisionMutation) AddedApplied() (r int, exists bool) {
+	v := m.addapplied
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// ResetApplied resets all changes to the "applied" field.
+func (m *RevisionMutation) ResetApplied() {
+	m.applied = nil
+	m.addapplied = nil
+}
+
+// SetTotal sets the "total" field.
+func (m *RevisionMutation) SetTotal(i int) {
+	m.total = &i
+	m.addtotal = nil
+}
+
+// Total returns the value of the "total" field in the mutation.
+func (m *RevisionMutation) Total() (r int, exists bool) {
+	v := m.total
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldTotal returns the old "total" 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) OldTotal(ctx context.Context) (v int, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldTotal is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldTotal requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldTotal: %w", err)
+	}
+	return oldValue.Total, nil
+}
+
+// AddTotal adds i to the "total" field.
+func (m *RevisionMutation) AddTotal(i int) {
+	if m.addtotal != nil {
+		*m.addtotal += i
+	} else {
+		m.addtotal = &i
+	}
+}
+
+// AddedTotal returns the value that was added to the "total" field in this mutation.
+func (m *RevisionMutation) AddedTotal() (r int, exists bool) {
+	v := m.addtotal
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// ResetTotal resets all changes to the "total" field.
+func (m *RevisionMutation) ResetTotal() {
+	m.total = nil
+	m.addtotal = nil
 }
 
 // SetExecutedAt sets the "executed_at" field.
@@ -319,6 +399,55 @@ func (m *RevisionMutation) ResetExecutionTime() {
 	m.addexecution_time = nil
 }
 
+// SetError sets the "error" field.
+func (m *RevisionMutation) SetError(s string) {
+	m.error = &s
+}
+
+// Error returns the value of the "error" field in the mutation.
+func (m *RevisionMutation) Error() (r string, exists bool) {
+	v := m.error
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldError returns the old "error" 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) OldError(ctx context.Context) (v string, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldError is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldError requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldError: %w", err)
+	}
+	return oldValue.Error, nil
+}
+
+// ClearError clears the value of the "error" field.
+func (m *RevisionMutation) ClearError() {
+	m.error = nil
+	m.clearedFields[revision.FieldError] = struct{}{}
+}
+
+// ErrorCleared returns if the "error" field was cleared in this mutation.
+func (m *RevisionMutation) ErrorCleared() bool {
+	_, ok := m.clearedFields[revision.FieldError]
+	return ok
+}
+
+// ResetError resets all changes to the "error" field.
+func (m *RevisionMutation) ResetError() {
+	m.error = nil
+	delete(m.clearedFields, revision.FieldError)
+}
+
 // SetHash sets the "hash" field.
 func (m *RevisionMutation) SetHash(s string) {
 	m.hash = &s
@@ -446,12 +575,15 @@ 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, 7)
+	fields := make([]string, 0, 9)
 	if m.description != nil {
 		fields = append(fields, revision.FieldDescription)
 	}
-	if m.execution_state != nil {
-		fields = append(fields, revision.FieldExecutionState)
+	if m.applied != nil {
+		fields = append(fields, revision.FieldApplied)
+	}
+	if m.total != nil {
+		fields = append(fields, revision.FieldTotal)
 	}
 	if m.executed_at != nil {
 		fields = append(fields, revision.FieldExecutedAt)
@@ -459,6 +591,9 @@ func (m *RevisionMutation) Fields() []string {
 	if m.execution_time != nil {
 		fields = append(fields, revision.FieldExecutionTime)
 	}
+	if m.error != nil {
+		fields = append(fields, revision.FieldError)
+	}
 	if m.hash != nil {
 		fields = append(fields, revision.FieldHash)
 	}
@@ -478,12 +613,16 @@ func (m *RevisionMutation) Field(name string) (ent.Value, bool) {
 	switch name {
 	case revision.FieldDescription:
 		return m.Description()
-	case revision.FieldExecutionState:
-		return m.ExecutionState()
+	case revision.FieldApplied:
+		return m.Applied()
+	case revision.FieldTotal:
+		return m.Total()
 	case revision.FieldExecutedAt:
 		return m.ExecutedAt()
 	case revision.FieldExecutionTime:
 		return m.ExecutionTime()
+	case revision.FieldError:
+		return m.Error()
 	case revision.FieldHash:
 		return m.Hash()
 	case revision.FieldOperatorVersion:
@@ -501,12 +640,16 @@ func (m *RevisionMutation) OldField(ctx context.Context, name string) (ent.Value
 	switch name {
 	case revision.FieldDescription:
 		return m.OldDescription(ctx)
-	case revision.FieldExecutionState:
-		return m.OldExecutionState(ctx)
+	case revision.FieldApplied:
+		return m.OldApplied(ctx)
+	case revision.FieldTotal:
+		return m.OldTotal(ctx)
 	case revision.FieldExecutedAt:
 		return m.OldExecutedAt(ctx)
 	case revision.FieldExecutionTime:
 		return m.OldExecutionTime(ctx)
+	case revision.FieldError:
+		return m.OldError(ctx)
 	case revision.FieldHash:
 		return m.OldHash(ctx)
 	case revision.FieldOperatorVersion:
@@ -529,12 +672,19 @@ func (m *RevisionMutation) SetField(name string, value ent.Value) error {
 		}
 		m.SetDescription(v)
 		return nil
-	case revision.FieldExecutionState:
-		v, ok := value.(revision.ExecutionState)
+	case revision.FieldApplied:
+		v, ok := value.(int)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetApplied(v)
+		return nil
+	case revision.FieldTotal:
+		v, ok := value.(int)
 		if !ok {
 			return fmt.Errorf("unexpected type %T for field %s", value, name)
 		}
-		m.SetExecutionState(v)
+		m.SetTotal(v)
 		return nil
 	case revision.FieldExecutedAt:
 		v, ok := value.(time.Time)
@@ -550,6 +700,13 @@ func (m *RevisionMutation) SetField(name string, value ent.Value) error {
 		}
 		m.SetExecutionTime(v)
 		return nil
+	case revision.FieldError:
+		v, ok := value.(string)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetError(v)
+		return nil
 	case revision.FieldHash:
 		v, ok := value.(string)
 		if !ok {
@@ -579,6 +736,12 @@ func (m *RevisionMutation) SetField(name string, value ent.Value) error {
 // this mutation.
 func (m *RevisionMutation) AddedFields() []string {
 	var fields []string
+	if m.addapplied != nil {
+		fields = append(fields, revision.FieldApplied)
+	}
+	if m.addtotal != nil {
+		fields = append(fields, revision.FieldTotal)
+	}
 	if m.addexecution_time != nil {
 		fields = append(fields, revision.FieldExecutionTime)
 	}
@@ -590,6 +753,10 @@ func (m *RevisionMutation) AddedFields() []string {
 // was not set, or was not defined in the schema.
 func (m *RevisionMutation) AddedField(name string) (ent.Value, bool) {
 	switch name {
+	case revision.FieldApplied:
+		return m.AddedApplied()
+	case revision.FieldTotal:
+		return m.AddedTotal()
 	case revision.FieldExecutionTime:
 		return m.AddedExecutionTime()
 	}
@@ -601,6 +768,20 @@ func (m *RevisionMutation) AddedField(name string) (ent.Value, bool) {
 // type.
 func (m *RevisionMutation) AddField(name string, value ent.Value) error {
 	switch name {
+	case revision.FieldApplied:
+		v, ok := value.(int)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.AddApplied(v)
+		return nil
+	case revision.FieldTotal:
+		v, ok := value.(int)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.AddTotal(v)
+		return nil
 	case revision.FieldExecutionTime:
 		v, ok := value.(time.Duration)
 		if !ok {
@@ -615,7 +796,11 @@ func (m *RevisionMutation) AddField(name string, value ent.Value) error {
 // ClearedFields returns all nullable fields that were cleared during this
 // mutation.
 func (m *RevisionMutation) ClearedFields() []string {
-	return nil
+	var fields []string
+	if m.FieldCleared(revision.FieldError) {
+		fields = append(fields, revision.FieldError)
+	}
+	return fields
 }
 
 // FieldCleared returns a boolean indicating if a field with the given name was
@@ -628,6 +813,11 @@ func (m *RevisionMutation) FieldCleared(name string) bool {
 // ClearField clears the value of the field with the given name. It returns an
 // error if the field is not defined in the schema.
 func (m *RevisionMutation) ClearField(name string) error {
+	switch name {
+	case revision.FieldError:
+		m.ClearError()
+		return nil
+	}
 	return fmt.Errorf("unknown Revision nullable field %s", name)
 }
 
@@ -638,8 +828,11 @@ func (m *RevisionMutation) ResetField(name string) error {
 	case revision.FieldDescription:
 		m.ResetDescription()
 		return nil
-	case revision.FieldExecutionState:
-		m.ResetExecutionState()
+	case revision.FieldApplied:
+		m.ResetApplied()
+		return nil
+	case revision.FieldTotal:
+		m.ResetTotal()
 		return nil
 	case revision.FieldExecutedAt:
 		m.ResetExecutedAt()
@@ -647,6 +840,9 @@ func (m *RevisionMutation) ResetField(name string) error {
 	case revision.FieldExecutionTime:
 		m.ResetExecutionTime()
 		return nil
+	case revision.FieldError:
+		m.ResetError()
+		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 dc777420..3b5bb679 100644
--- a/cmd/atlas/internal/migrate/ent/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision.go
@@ -23,12 +23,16 @@ type Revision struct {
 	ID string `json:"id,omitempty"`
 	// Description holds the value of the "description" field.
 	Description string `json:"description,omitempty"`
-	// ExecutionState holds the value of the "execution_state" field.
-	ExecutionState revision.ExecutionState `json:"execution_state,omitempty"`
+	// Applied holds the value of the "applied" field.
+	Applied int `json:"applied,omitempty"`
+	// Total holds the value of the "total" field.
+	Total int `json:"total,omitempty"`
 	// ExecutedAt holds the value of the "executed_at" field.
 	ExecutedAt time.Time `json:"executed_at,omitempty"`
 	// ExecutionTime holds the value of the "execution_time" field.
 	ExecutionTime time.Duration `json:"execution_time,omitempty"`
+	// Error holds the value of the "error" field.
+	Error string `json:"error,omitempty"`
 	// Hash holds the value of the "hash" field.
 	Hash string `json:"hash,omitempty"`
 	// OperatorVersion holds the value of the "operator_version" field.
@@ -44,9 +48,9 @@ func (*Revision) scanValues(columns []string) ([]interface{}, error) {
 		switch columns[i] {
 		case revision.FieldMeta:
 			values[i] = new([]byte)
-		case revision.FieldExecutionTime:
+		case revision.FieldApplied, revision.FieldTotal, revision.FieldExecutionTime:
 			values[i] = new(sql.NullInt64)
-		case revision.FieldID, revision.FieldDescription, revision.FieldExecutionState, revision.FieldHash, revision.FieldOperatorVersion:
+		case revision.FieldID, revision.FieldDescription, revision.FieldError, revision.FieldHash, revision.FieldOperatorVersion:
 			values[i] = new(sql.NullString)
 		case revision.FieldExecutedAt:
 			values[i] = new(sql.NullTime)
@@ -77,11 +81,17 @@ func (r *Revision) assignValues(columns []string, values []interface{}) error {
 			} else if value.Valid {
 				r.Description = value.String
 			}
-		case revision.FieldExecutionState:
-			if value, ok := values[i].(*sql.NullString); !ok {
-				return fmt.Errorf("unexpected type %T for field execution_state", values[i])
+		case revision.FieldApplied:
+			if value, ok := values[i].(*sql.NullInt64); !ok {
+				return fmt.Errorf("unexpected type %T for field applied", values[i])
 			} else if value.Valid {
-				r.ExecutionState = revision.ExecutionState(value.String)
+				r.Applied = int(value.Int64)
+			}
+		case revision.FieldTotal:
+			if value, ok := values[i].(*sql.NullInt64); !ok {
+				return fmt.Errorf("unexpected type %T for field total", values[i])
+			} else if value.Valid {
+				r.Total = int(value.Int64)
 			}
 		case revision.FieldExecutedAt:
 			if value, ok := values[i].(*sql.NullTime); !ok {
@@ -95,6 +105,12 @@ func (r *Revision) assignValues(columns []string, values []interface{}) error {
 			} else if value.Valid {
 				r.ExecutionTime = time.Duration(value.Int64)
 			}
+		case revision.FieldError:
+			if value, ok := values[i].(*sql.NullString); !ok {
+				return fmt.Errorf("unexpected type %T for field error", values[i])
+			} else if value.Valid {
+				r.Error = value.String
+			}
 		case revision.FieldHash:
 			if value, ok := values[i].(*sql.NullString); !ok {
 				return fmt.Errorf("unexpected type %T for field hash", values[i])
@@ -130,11 +146,11 @@ func (r *Revision) Update() *RevisionUpdateOne {
 // Unwrap unwraps the Revision entity that was returned from a transaction after it was closed,
 // so that all future queries will be executed through the driver which created the transaction.
 func (r *Revision) Unwrap() *Revision {
-	tx, ok := r.config.driver.(*txDriver)
+	_tx, ok := r.config.driver.(*txDriver)
 	if !ok {
 		panic("ent: Revision is not a transactional entity")
 	}
-	r.config.driver = tx.drv
+	r.config.driver = _tx.drv
 	return r
 }
 
@@ -142,20 +158,32 @@ func (r *Revision) Unwrap() *Revision {
 func (r *Revision) String() string {
 	var builder strings.Builder
 	builder.WriteString("Revision(")
-	builder.WriteString(fmt.Sprintf("id=%v", r.ID))
-	builder.WriteString(", description=")
+	builder.WriteString(fmt.Sprintf("id=%v, ", r.ID))
+	builder.WriteString("description=")
 	builder.WriteString(r.Description)
-	builder.WriteString(", execution_state=")
-	builder.WriteString(fmt.Sprintf("%v", r.ExecutionState))
-	builder.WriteString(", executed_at=")
+	builder.WriteString(", ")
+	builder.WriteString("applied=")
+	builder.WriteString(fmt.Sprintf("%v", r.Applied))
+	builder.WriteString(", ")
+	builder.WriteString("total=")
+	builder.WriteString(fmt.Sprintf("%v", r.Total))
+	builder.WriteString(", ")
+	builder.WriteString("executed_at=")
 	builder.WriteString(r.ExecutedAt.Format(time.ANSIC))
-	builder.WriteString(", execution_time=")
+	builder.WriteString(", ")
+	builder.WriteString("execution_time=")
 	builder.WriteString(fmt.Sprintf("%v", r.ExecutionTime))
-	builder.WriteString(", hash=")
+	builder.WriteString(", ")
+	builder.WriteString("error=")
+	builder.WriteString(r.Error)
+	builder.WriteString(", ")
+	builder.WriteString("hash=")
 	builder.WriteString(r.Hash)
-	builder.WriteString(", operator_version=")
+	builder.WriteString(", ")
+	builder.WriteString("operator_version=")
 	builder.WriteString(r.OperatorVersion)
-	builder.WriteString(", meta=")
+	builder.WriteString(", ")
+	builder.WriteString("meta=")
 	builder.WriteString(fmt.Sprintf("%v", r.Meta))
 	builder.WriteByte(')')
 	return builder.String()
diff --git a/cmd/atlas/internal/migrate/ent/revision/revision.go b/cmd/atlas/internal/migrate/ent/revision/revision.go
index 8486e2da..7b186758 100644
--- a/cmd/atlas/internal/migrate/ent/revision/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision/revision.go
@@ -6,10 +6,6 @@
 
 package revision
 
-import (
-	"fmt"
-)
-
 const (
 	// Label holds the string label denoting the revision type in the database.
 	Label = "revision"
@@ -17,12 +13,16 @@ const (
 	FieldID = "version"
 	// FieldDescription holds the string denoting the description field in the database.
 	FieldDescription = "description"
-	// FieldExecutionState holds the string denoting the execution_state field in the database.
-	FieldExecutionState = "execution_state"
+	// FieldApplied holds the string denoting the applied field in the database.
+	FieldApplied = "applied"
+	// FieldTotal holds the string denoting the total field in the database.
+	FieldTotal = "total"
 	// FieldExecutedAt holds the string denoting the executed_at field in the database.
 	FieldExecutedAt = "executed_at"
 	// FieldExecutionTime holds the string denoting the execution_time field in the database.
 	FieldExecutionTime = "execution_time"
+	// FieldError holds the string denoting the error field in the database.
+	FieldError = "error"
 	// FieldHash holds the string denoting the hash field in the database.
 	FieldHash = "hash"
 	// FieldOperatorVersion holds the string denoting the operator_version field in the database.
@@ -37,9 +37,11 @@ const (
 var Columns = []string{
 	FieldID,
 	FieldDescription,
-	FieldExecutionState,
+	FieldApplied,
+	FieldTotal,
 	FieldExecutedAt,
 	FieldExecutionTime,
+	FieldError,
 	FieldHash,
 	FieldOperatorVersion,
 	FieldMeta,
@@ -55,26 +57,7 @@ func ValidColumn(column string) bool {
 	return false
 }
 
-// ExecutionState defines the type for the "execution_state" enum field.
-type ExecutionState string
-
-// ExecutionState values.
-const (
-	ExecutionStateOngoing ExecutionState = "ongoing"
-	ExecutionStateOk      ExecutionState = "ok"
-	ExecutionStateError   ExecutionState = "error"
+var (
+	// TotalValidator is a validator for the "total" field. It is called by the builders before save.
+	TotalValidator func(int) error
 )
-
-func (es ExecutionState) String() string {
-	return string(es)
-}
-
-// ExecutionStateValidator is a validator for the "execution_state" field enum values. It is called by the builders before save.
-func ExecutionStateValidator(es ExecutionState) error {
-	switch es {
-	case ExecutionStateOngoing, ExecutionStateOk, ExecutionStateError:
-		return nil
-	default:
-		return fmt.Errorf("revision: invalid enum value for execution_state field: %q", es)
-	}
-}
diff --git a/cmd/atlas/internal/migrate/ent/revision/where.go b/cmd/atlas/internal/migrate/ent/revision/where.go
index 1e8e5412..53846132 100644
--- a/cmd/atlas/internal/migrate/ent/revision/where.go
+++ b/cmd/atlas/internal/migrate/ent/revision/where.go
@@ -37,12 +37,6 @@ func IDNEQ(id string) predicate.Revision {
 // IDIn applies the In predicate on the ID field.
 func IDIn(ids ...string) predicate.Revision {
 	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(ids) == 0 {
-			s.Where(sql.False())
-			return
-		}
 		v := make([]interface{}, len(ids))
 		for i := range v {
 			v[i] = ids[i]
@@ -54,12 +48,6 @@ func IDIn(ids ...string) predicate.Revision {
 // IDNotIn applies the NotIn predicate on the ID field.
 func IDNotIn(ids ...string) predicate.Revision {
 	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(ids) == 0 {
-			s.Where(sql.False())
-			return
-		}
 		v := make([]interface{}, len(ids))
 		for i := range v {
 			v[i] = ids[i]
@@ -103,6 +91,20 @@ func Description(v string) predicate.Revision {
 	})
 }
 
+// Applied applies equality check predicate on the "applied" field. It's identical to AppliedEQ.
+func Applied(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldApplied), v))
+	})
+}
+
+// Total applies equality check predicate on the "total" field. It's identical to TotalEQ.
+func Total(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldTotal), v))
+	})
+}
+
 // ExecutedAt applies equality check predicate on the "executed_at" field. It's identical to ExecutedAtEQ.
 func ExecutedAt(v time.Time) predicate.Revision {
 	return predicate.Revision(func(s *sql.Selector) {
@@ -118,6 +120,13 @@ func ExecutionTime(v time.Duration) predicate.Revision {
 	})
 }
 
+// Error applies equality check predicate on the "error" field. It's identical to ErrorEQ.
+func Error(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldError), 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) {
@@ -243,22 +252,22 @@ func DescriptionContainsFold(v string) predicate.Revision {
 	})
 }
 
-// ExecutionStateEQ applies the EQ predicate on the "execution_state" field.
-func ExecutionStateEQ(v ExecutionState) predicate.Revision {
+// AppliedEQ applies the EQ predicate on the "applied" field.
+func AppliedEQ(v int) predicate.Revision {
 	return predicate.Revision(func(s *sql.Selector) {
-		s.Where(sql.EQ(s.C(FieldExecutionState), v))
+		s.Where(sql.EQ(s.C(FieldApplied), v))
 	})
 }
 
-// ExecutionStateNEQ applies the NEQ predicate on the "execution_state" field.
-func ExecutionStateNEQ(v ExecutionState) predicate.Revision {
+// AppliedNEQ applies the NEQ predicate on the "applied" field.
+func AppliedNEQ(v int) predicate.Revision {
 	return predicate.Revision(func(s *sql.Selector) {
-		s.Where(sql.NEQ(s.C(FieldExecutionState), v))
+		s.Where(sql.NEQ(s.C(FieldApplied), v))
 	})
 }
 
-// ExecutionStateIn applies the In predicate on the "execution_state" field.
-func ExecutionStateIn(vs ...ExecutionState) predicate.Revision {
+// AppliedIn applies the In predicate on the "applied" field.
+func AppliedIn(vs ...int) predicate.Revision {
 	v := make([]interface{}, len(vs))
 	for i := range v {
 		v[i] = vs[i]
@@ -270,12 +279,12 @@ func ExecutionStateIn(vs ...ExecutionState) predicate.Revision {
 			s.Where(sql.False())
 			return
 		}
-		s.Where(sql.In(s.C(FieldExecutionState), v...))
+		s.Where(sql.In(s.C(FieldApplied), v...))
 	})
 }
 
-// ExecutionStateNotIn applies the NotIn predicate on the "execution_state" field.
-func ExecutionStateNotIn(vs ...ExecutionState) predicate.Revision {
+// AppliedNotIn applies the NotIn predicate on the "applied" field.
+func AppliedNotIn(vs ...int) predicate.Revision {
 	v := make([]interface{}, len(vs))
 	for i := range v {
 		v[i] = vs[i]
@@ -287,7 +296,111 @@ func ExecutionStateNotIn(vs ...ExecutionState) predicate.Revision {
 			s.Where(sql.False())
 			return
 		}
-		s.Where(sql.NotIn(s.C(FieldExecutionState), v...))
+		s.Where(sql.NotIn(s.C(FieldApplied), v...))
+	})
+}
+
+// AppliedGT applies the GT predicate on the "applied" field.
+func AppliedGT(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldApplied), v))
+	})
+}
+
+// AppliedGTE applies the GTE predicate on the "applied" field.
+func AppliedGTE(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldApplied), v))
+	})
+}
+
+// AppliedLT applies the LT predicate on the "applied" field.
+func AppliedLT(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldApplied), v))
+	})
+}
+
+// AppliedLTE applies the LTE predicate on the "applied" field.
+func AppliedLTE(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldApplied), v))
+	})
+}
+
+// TotalEQ applies the EQ predicate on the "total" field.
+func TotalEQ(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldTotal), v))
+	})
+}
+
+// TotalNEQ applies the NEQ predicate on the "total" field.
+func TotalNEQ(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.NEQ(s.C(FieldTotal), v))
+	})
+}
+
+// TotalIn applies the In predicate on the "total" field.
+func TotalIn(vs ...int) 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(FieldTotal), v...))
+	})
+}
+
+// TotalNotIn applies the NotIn predicate on the "total" field.
+func TotalNotIn(vs ...int) 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(FieldTotal), v...))
+	})
+}
+
+// TotalGT applies the GT predicate on the "total" field.
+func TotalGT(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldTotal), v))
+	})
+}
+
+// TotalGTE applies the GTE predicate on the "total" field.
+func TotalGTE(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldTotal), v))
+	})
+}
+
+// TotalLT applies the LT predicate on the "total" field.
+func TotalLT(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldTotal), v))
+	})
+}
+
+// TotalLTE applies the LTE predicate on the "total" field.
+func TotalLTE(v int) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldTotal), v))
 	})
 }
 
@@ -449,6 +562,131 @@ func ExecutionTimeLTE(v time.Duration) predicate.Revision {
 	})
 }
 
+// ErrorEQ applies the EQ predicate on the "error" field.
+func ErrorEQ(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EQ(s.C(FieldError), v))
+	})
+}
+
+// ErrorNEQ applies the NEQ predicate on the "error" field.
+func ErrorNEQ(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.NEQ(s.C(FieldError), v))
+	})
+}
+
+// ErrorIn applies the In predicate on the "error" field.
+func ErrorIn(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(FieldError), v...))
+	})
+}
+
+// ErrorNotIn applies the NotIn predicate on the "error" field.
+func ErrorNotIn(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(FieldError), v...))
+	})
+}
+
+// ErrorGT applies the GT predicate on the "error" field.
+func ErrorGT(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GT(s.C(FieldError), v))
+	})
+}
+
+// ErrorGTE applies the GTE predicate on the "error" field.
+func ErrorGTE(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.GTE(s.C(FieldError), v))
+	})
+}
+
+// ErrorLT applies the LT predicate on the "error" field.
+func ErrorLT(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LT(s.C(FieldError), v))
+	})
+}
+
+// ErrorLTE applies the LTE predicate on the "error" field.
+func ErrorLTE(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.LTE(s.C(FieldError), v))
+	})
+}
+
+// ErrorContains applies the Contains predicate on the "error" field.
+func ErrorContains(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.Contains(s.C(FieldError), v))
+	})
+}
+
+// ErrorHasPrefix applies the HasPrefix predicate on the "error" field.
+func ErrorHasPrefix(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.HasPrefix(s.C(FieldError), v))
+	})
+}
+
+// ErrorHasSuffix applies the HasSuffix predicate on the "error" field.
+func ErrorHasSuffix(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.HasSuffix(s.C(FieldError), v))
+	})
+}
+
+// ErrorIsNil applies the IsNil predicate on the "error" field.
+func ErrorIsNil() predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.IsNull(s.C(FieldError)))
+	})
+}
+
+// ErrorNotNil applies the NotNil predicate on the "error" field.
+func ErrorNotNil() predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.NotNull(s.C(FieldError)))
+	})
+}
+
+// ErrorEqualFold applies the EqualFold predicate on the "error" field.
+func ErrorEqualFold(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.EqualFold(s.C(FieldError), v))
+	})
+}
+
+// ErrorContainsFold applies the ContainsFold predicate on the "error" field.
+func ErrorContainsFold(v string) predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.ContainsFold(s.C(FieldError), 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 364d4f95..84cc17ad 100644
--- a/cmd/atlas/internal/migrate/ent/revision_create.go
+++ b/cmd/atlas/internal/migrate/ent/revision_create.go
@@ -33,9 +33,15 @@ func (rc *RevisionCreate) SetDescription(s string) *RevisionCreate {
 	return rc
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (rc *RevisionCreate) SetExecutionState(rs revision.ExecutionState) *RevisionCreate {
-	rc.mutation.SetExecutionState(rs)
+// SetApplied sets the "applied" field.
+func (rc *RevisionCreate) SetApplied(i int) *RevisionCreate {
+	rc.mutation.SetApplied(i)
+	return rc
+}
+
+// SetTotal sets the "total" field.
+func (rc *RevisionCreate) SetTotal(i int) *RevisionCreate {
+	rc.mutation.SetTotal(i)
 	return rc
 }
 
@@ -51,6 +57,20 @@ func (rc *RevisionCreate) SetExecutionTime(t time.Duration) *RevisionCreate {
 	return rc
 }
 
+// SetError sets the "error" field.
+func (rc *RevisionCreate) SetError(s string) *RevisionCreate {
+	rc.mutation.SetError(s)
+	return rc
+}
+
+// SetNillableError sets the "error" field if the given value is not nil.
+func (rc *RevisionCreate) SetNillableError(s *string) *RevisionCreate {
+	if s != nil {
+		rc.SetError(*s)
+	}
+	return rc
+}
+
 // SetHash sets the "hash" field.
 func (rc *RevisionCreate) SetHash(s string) *RevisionCreate {
 	rc.mutation.SetHash(s)
@@ -114,9 +134,15 @@ func (rc *RevisionCreate) Save(ctx context.Context) (*Revision, error) {
 			}
 			mut = rc.hooks[i](mut)
 		}
-		if _, err := mut.Mutate(ctx, rc.mutation); err != nil {
+		v, err := mut.Mutate(ctx, rc.mutation)
+		if err != nil {
 			return nil, err
 		}
+		nv, ok := v.(*Revision)
+		if !ok {
+			return nil, fmt.Errorf("unexpected node type %T returned from RevisionMutation", v)
+		}
+		node = nv
 	}
 	return node, err
 }
@@ -148,12 +174,15 @@ func (rc *RevisionCreate) check() error {
 	if _, ok := rc.mutation.Description(); !ok {
 		return &ValidationError{Name: "description", err: errors.New(`ent: missing required field "Revision.description"`)}
 	}
-	if _, ok := rc.mutation.ExecutionState(); !ok {
-		return &ValidationError{Name: "execution_state", err: errors.New(`ent: missing required field "Revision.execution_state"`)}
+	if _, ok := rc.mutation.Applied(); !ok {
+		return &ValidationError{Name: "applied", err: errors.New(`ent: missing required field "Revision.applied"`)}
+	}
+	if _, ok := rc.mutation.Total(); !ok {
+		return &ValidationError{Name: "total", err: errors.New(`ent: missing required field "Revision.total"`)}
 	}
-	if v, ok := rc.mutation.ExecutionState(); ok {
-		if err := revision.ExecutionStateValidator(v); err != nil {
-			return &ValidationError{Name: "execution_state", err: fmt.Errorf(`ent: validator failed for field "Revision.execution_state": %w`, err)}
+	if v, ok := rc.mutation.Total(); ok {
+		if err := revision.TotalValidator(v); err != nil {
+			return &ValidationError{Name: "total", err: fmt.Errorf(`ent: validator failed for field "Revision.total": %w`, err)}
 		}
 	}
 	if _, ok := rc.mutation.ExecutedAt(); !ok {
@@ -178,7 +207,7 @@ func (rc *RevisionCreate) sqlSave(ctx context.Context) (*Revision, error) {
 	_node, _spec := rc.createSpec()
 	if err := sqlgraph.CreateNode(ctx, rc.driver, _spec); err != nil {
 		if sqlgraph.IsConstraintError(err) {
-			err = &ConstraintError{err.Error(), err}
+			err = &ConstraintError{msg: err.Error(), wrap: err}
 		}
 		return nil, err
 	}
@@ -217,13 +246,21 @@ func (rc *RevisionCreate) createSpec() (*Revision, *sqlgraph.CreateSpec) {
 		})
 		_node.Description = value
 	}
-	if value, ok := rc.mutation.ExecutionState(); ok {
+	if value, ok := rc.mutation.Applied(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeInt,
+			Value:  value,
+			Column: revision.FieldApplied,
+		})
+		_node.Applied = value
+	}
+	if value, ok := rc.mutation.Total(); ok {
 		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
-			Type:   field.TypeEnum,
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldExecutionState,
+			Column: revision.FieldTotal,
 		})
-		_node.ExecutionState = value
+		_node.Total = value
 	}
 	if value, ok := rc.mutation.ExecutedAt(); ok {
 		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
@@ -241,6 +278,14 @@ func (rc *RevisionCreate) createSpec() (*Revision, *sqlgraph.CreateSpec) {
 		})
 		_node.ExecutionTime = value
 	}
+	if value, ok := rc.mutation.Error(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: revision.FieldError,
+		})
+		_node.Error = value
+	}
 	if value, ok := rc.mutation.Hash(); ok {
 		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -331,15 +376,39 @@ func (u *RevisionUpsert) UpdateDescription() *RevisionUpsert {
 	return u
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (u *RevisionUpsert) SetExecutionState(v revision.ExecutionState) *RevisionUpsert {
-	u.Set(revision.FieldExecutionState, v)
+// SetApplied sets the "applied" field.
+func (u *RevisionUpsert) SetApplied(v int) *RevisionUpsert {
+	u.Set(revision.FieldApplied, v)
+	return u
+}
+
+// UpdateApplied sets the "applied" field to the value that was provided on create.
+func (u *RevisionUpsert) UpdateApplied() *RevisionUpsert {
+	u.SetExcluded(revision.FieldApplied)
 	return u
 }
 
-// UpdateExecutionState sets the "execution_state" field to the value that was provided on create.
-func (u *RevisionUpsert) UpdateExecutionState() *RevisionUpsert {
-	u.SetExcluded(revision.FieldExecutionState)
+// AddApplied adds v to the "applied" field.
+func (u *RevisionUpsert) AddApplied(v int) *RevisionUpsert {
+	u.Add(revision.FieldApplied, v)
+	return u
+}
+
+// SetTotal sets the "total" field.
+func (u *RevisionUpsert) SetTotal(v int) *RevisionUpsert {
+	u.Set(revision.FieldTotal, v)
+	return u
+}
+
+// UpdateTotal sets the "total" field to the value that was provided on create.
+func (u *RevisionUpsert) UpdateTotal() *RevisionUpsert {
+	u.SetExcluded(revision.FieldTotal)
+	return u
+}
+
+// AddTotal adds v to the "total" field.
+func (u *RevisionUpsert) AddTotal(v int) *RevisionUpsert {
+	u.Add(revision.FieldTotal, v)
 	return u
 }
 
@@ -373,6 +442,24 @@ func (u *RevisionUpsert) AddExecutionTime(v time.Duration) *RevisionUpsert {
 	return u
 }
 
+// SetError sets the "error" field.
+func (u *RevisionUpsert) SetError(v string) *RevisionUpsert {
+	u.Set(revision.FieldError, v)
+	return u
+}
+
+// UpdateError sets the "error" field to the value that was provided on create.
+func (u *RevisionUpsert) UpdateError() *RevisionUpsert {
+	u.SetExcluded(revision.FieldError)
+	return u
+}
+
+// ClearError clears the value of the "error" field.
+func (u *RevisionUpsert) ClearError() *RevisionUpsert {
+	u.SetNull(revision.FieldError)
+	return u
+}
+
 // SetHash sets the "hash" field.
 func (u *RevisionUpsert) SetHash(v string) *RevisionUpsert {
 	u.Set(revision.FieldHash, v)
@@ -427,6 +514,12 @@ func (u *RevisionUpsertOne) UpdateNewValues() *RevisionUpsertOne {
 		if _, exists := u.create.mutation.ID(); exists {
 			s.SetIgnore(revision.FieldID)
 		}
+		if _, exists := u.create.mutation.Description(); exists {
+			s.SetIgnore(revision.FieldDescription)
+		}
+		if _, exists := u.create.mutation.ExecutedAt(); exists {
+			s.SetIgnore(revision.FieldExecutedAt)
+		}
 	}))
 	return u
 }
@@ -473,17 +566,45 @@ func (u *RevisionUpsertOne) UpdateDescription() *RevisionUpsertOne {
 	})
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (u *RevisionUpsertOne) SetExecutionState(v revision.ExecutionState) *RevisionUpsertOne {
+// SetApplied sets the "applied" field.
+func (u *RevisionUpsertOne) SetApplied(v int) *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetApplied(v)
+	})
+}
+
+// AddApplied adds v to the "applied" field.
+func (u *RevisionUpsertOne) AddApplied(v int) *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.AddApplied(v)
+	})
+}
+
+// UpdateApplied sets the "applied" field to the value that was provided on create.
+func (u *RevisionUpsertOne) UpdateApplied() *RevisionUpsertOne {
 	return u.Update(func(s *RevisionUpsert) {
-		s.SetExecutionState(v)
+		s.UpdateApplied()
 	})
 }
 
-// UpdateExecutionState sets the "execution_state" field to the value that was provided on create.
-func (u *RevisionUpsertOne) UpdateExecutionState() *RevisionUpsertOne {
+// SetTotal sets the "total" field.
+func (u *RevisionUpsertOne) SetTotal(v int) *RevisionUpsertOne {
 	return u.Update(func(s *RevisionUpsert) {
-		s.UpdateExecutionState()
+		s.SetTotal(v)
+	})
+}
+
+// AddTotal adds v to the "total" field.
+func (u *RevisionUpsertOne) AddTotal(v int) *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.AddTotal(v)
+	})
+}
+
+// UpdateTotal sets the "total" field to the value that was provided on create.
+func (u *RevisionUpsertOne) UpdateTotal() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateTotal()
 	})
 }
 
@@ -522,6 +643,27 @@ func (u *RevisionUpsertOne) UpdateExecutionTime() *RevisionUpsertOne {
 	})
 }
 
+// SetError sets the "error" field.
+func (u *RevisionUpsertOne) SetError(v string) *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetError(v)
+	})
+}
+
+// UpdateError sets the "error" field to the value that was provided on create.
+func (u *RevisionUpsertOne) UpdateError() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateError()
+	})
+}
+
+// ClearError clears the value of the "error" field.
+func (u *RevisionUpsertOne) ClearError() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.ClearError()
+	})
+}
+
 // SetHash sets the "hash" field.
 func (u *RevisionUpsertOne) SetHash(v string) *RevisionUpsertOne {
 	return u.Update(func(s *RevisionUpsert) {
@@ -636,7 +778,7 @@ func (rcb *RevisionCreateBulk) Save(ctx context.Context) ([]*Revision, error) {
 					// Invoke the actual operation on the latest mutation in the chain.
 					if err = sqlgraph.BatchCreate(ctx, rcb.driver, spec); err != nil {
 						if sqlgraph.IsConstraintError(err) {
-							err = &ConstraintError{err.Error(), err}
+							err = &ConstraintError{msg: err.Error(), wrap: err}
 						}
 					}
 				}
@@ -746,6 +888,12 @@ func (u *RevisionUpsertBulk) UpdateNewValues() *RevisionUpsertBulk {
 				s.SetIgnore(revision.FieldID)
 				return
 			}
+			if _, exists := b.mutation.Description(); exists {
+				s.SetIgnore(revision.FieldDescription)
+			}
+			if _, exists := b.mutation.ExecutedAt(); exists {
+				s.SetIgnore(revision.FieldExecutedAt)
+			}
 		}
 	}))
 	return u
@@ -793,17 +941,45 @@ func (u *RevisionUpsertBulk) UpdateDescription() *RevisionUpsertBulk {
 	})
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (u *RevisionUpsertBulk) SetExecutionState(v revision.ExecutionState) *RevisionUpsertBulk {
+// SetApplied sets the "applied" field.
+func (u *RevisionUpsertBulk) SetApplied(v int) *RevisionUpsertBulk {
 	return u.Update(func(s *RevisionUpsert) {
-		s.SetExecutionState(v)
+		s.SetApplied(v)
 	})
 }
 
-// UpdateExecutionState sets the "execution_state" field to the value that was provided on create.
-func (u *RevisionUpsertBulk) UpdateExecutionState() *RevisionUpsertBulk {
+// AddApplied adds v to the "applied" field.
+func (u *RevisionUpsertBulk) AddApplied(v int) *RevisionUpsertBulk {
 	return u.Update(func(s *RevisionUpsert) {
-		s.UpdateExecutionState()
+		s.AddApplied(v)
+	})
+}
+
+// UpdateApplied sets the "applied" field to the value that was provided on create.
+func (u *RevisionUpsertBulk) UpdateApplied() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateApplied()
+	})
+}
+
+// SetTotal sets the "total" field.
+func (u *RevisionUpsertBulk) SetTotal(v int) *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetTotal(v)
+	})
+}
+
+// AddTotal adds v to the "total" field.
+func (u *RevisionUpsertBulk) AddTotal(v int) *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.AddTotal(v)
+	})
+}
+
+// UpdateTotal sets the "total" field to the value that was provided on create.
+func (u *RevisionUpsertBulk) UpdateTotal() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateTotal()
 	})
 }
 
@@ -842,6 +1018,27 @@ func (u *RevisionUpsertBulk) UpdateExecutionTime() *RevisionUpsertBulk {
 	})
 }
 
+// SetError sets the "error" field.
+func (u *RevisionUpsertBulk) SetError(v string) *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetError(v)
+	})
+}
+
+// UpdateError sets the "error" field to the value that was provided on create.
+func (u *RevisionUpsertBulk) UpdateError() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdateError()
+	})
+}
+
+// ClearError clears the value of the "error" field.
+func (u *RevisionUpsertBulk) ClearError() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.ClearError()
+	})
+}
+
 // 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_delete.go b/cmd/atlas/internal/migrate/ent/revision_delete.go
index 2fadc842..1f706738 100644
--- a/cmd/atlas/internal/migrate/ent/revision_delete.go
+++ b/cmd/atlas/internal/migrate/ent/revision_delete.go
@@ -91,7 +91,11 @@ func (rd *RevisionDelete) sqlExec(ctx context.Context) (int, error) {
 			}
 		}
 	}
-	return sqlgraph.DeleteNodes(ctx, rd.driver, _spec)
+	affected, err := sqlgraph.DeleteNodes(ctx, rd.driver, _spec)
+	if err != nil && sqlgraph.IsConstraintError(err) {
+		err = &ConstraintError{msg: err.Error(), wrap: err}
+	}
+	return affected, err
 }
 
 // RevisionDeleteOne is the builder for deleting a single Revision entity.
diff --git a/cmd/atlas/internal/migrate/ent/revision_update.go b/cmd/atlas/internal/migrate/ent/revision_update.go
index 07aa5f76..1af508cd 100644
--- a/cmd/atlas/internal/migrate/ent/revision_update.go
+++ b/cmd/atlas/internal/migrate/ent/revision_update.go
@@ -33,21 +33,29 @@ func (ru *RevisionUpdate) Where(ps ...predicate.Revision) *RevisionUpdate {
 	return ru
 }
 
-// SetDescription sets the "description" field.
-func (ru *RevisionUpdate) SetDescription(s string) *RevisionUpdate {
-	ru.mutation.SetDescription(s)
+// SetApplied sets the "applied" field.
+func (ru *RevisionUpdate) SetApplied(i int) *RevisionUpdate {
+	ru.mutation.ResetApplied()
+	ru.mutation.SetApplied(i)
 	return ru
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (ru *RevisionUpdate) SetExecutionState(rs revision.ExecutionState) *RevisionUpdate {
-	ru.mutation.SetExecutionState(rs)
+// AddApplied adds i to the "applied" field.
+func (ru *RevisionUpdate) AddApplied(i int) *RevisionUpdate {
+	ru.mutation.AddApplied(i)
 	return ru
 }
 
-// SetExecutedAt sets the "executed_at" field.
-func (ru *RevisionUpdate) SetExecutedAt(t time.Time) *RevisionUpdate {
-	ru.mutation.SetExecutedAt(t)
+// SetTotal sets the "total" field.
+func (ru *RevisionUpdate) SetTotal(i int) *RevisionUpdate {
+	ru.mutation.ResetTotal()
+	ru.mutation.SetTotal(i)
+	return ru
+}
+
+// AddTotal adds i to the "total" field.
+func (ru *RevisionUpdate) AddTotal(i int) *RevisionUpdate {
+	ru.mutation.AddTotal(i)
 	return ru
 }
 
@@ -64,6 +72,26 @@ func (ru *RevisionUpdate) AddExecutionTime(t time.Duration) *RevisionUpdate {
 	return ru
 }
 
+// SetError sets the "error" field.
+func (ru *RevisionUpdate) SetError(s string) *RevisionUpdate {
+	ru.mutation.SetError(s)
+	return ru
+}
+
+// SetNillableError sets the "error" field if the given value is not nil.
+func (ru *RevisionUpdate) SetNillableError(s *string) *RevisionUpdate {
+	if s != nil {
+		ru.SetError(*s)
+	}
+	return ru
+}
+
+// ClearError clears the value of the "error" field.
+func (ru *RevisionUpdate) ClearError() *RevisionUpdate {
+	ru.mutation.ClearError()
+	return ru
+}
+
 // SetHash sets the "hash" field.
 func (ru *RevisionUpdate) SetHash(s string) *RevisionUpdate {
 	ru.mutation.SetHash(s)
@@ -149,9 +177,9 @@ func (ru *RevisionUpdate) ExecX(ctx context.Context) {
 
 // check runs all checks and user-defined validators on the builder.
 func (ru *RevisionUpdate) check() error {
-	if v, ok := ru.mutation.ExecutionState(); ok {
-		if err := revision.ExecutionStateValidator(v); err != nil {
-			return &ValidationError{Name: "execution_state", err: fmt.Errorf(`ent: validator failed for field "Revision.execution_state": %w`, err)}
+	if v, ok := ru.mutation.Total(); ok {
+		if err := revision.TotalValidator(v); err != nil {
+			return &ValidationError{Name: "total", err: fmt.Errorf(`ent: validator failed for field "Revision.total": %w`, err)}
 		}
 	}
 	return nil
@@ -175,25 +203,32 @@ func (ru *RevisionUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			}
 		}
 	}
-	if value, ok := ru.mutation.Description(); ok {
+	if value, ok := ru.mutation.Applied(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
-			Type:   field.TypeString,
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldDescription,
+			Column: revision.FieldApplied,
 		})
 	}
-	if value, ok := ru.mutation.ExecutionState(); ok {
-		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
-			Type:   field.TypeEnum,
+	if value, ok := ru.mutation.AddedApplied(); ok {
+		_spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldExecutionState,
+			Column: revision.FieldApplied,
 		})
 	}
-	if value, ok := ru.mutation.ExecutedAt(); ok {
+	if value, ok := ru.mutation.Total(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
-			Type:   field.TypeTime,
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldExecutedAt,
+			Column: revision.FieldTotal,
+		})
+	}
+	if value, ok := ru.mutation.AddedTotal(); ok {
+		_spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{
+			Type:   field.TypeInt,
+			Value:  value,
+			Column: revision.FieldTotal,
 		})
 	}
 	if value, ok := ru.mutation.ExecutionTime(); ok {
@@ -210,6 +245,19 @@ func (ru *RevisionUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			Column: revision.FieldExecutionTime,
 		})
 	}
+	if value, ok := ru.mutation.Error(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: revision.FieldError,
+		})
+	}
+	if ru.mutation.ErrorCleared() {
+		_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Column: revision.FieldError,
+		})
+	}
 	if value, ok := ru.mutation.Hash(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -237,7 +285,7 @@ func (ru *RevisionUpdate) sqlSave(ctx context.Context) (n int, err error) {
 		if _, ok := err.(*sqlgraph.NotFoundError); ok {
 			err = &NotFoundError{revision.Label}
 		} else if sqlgraph.IsConstraintError(err) {
-			err = &ConstraintError{err.Error(), err}
+			err = &ConstraintError{msg: err.Error(), wrap: err}
 		}
 		return 0, err
 	}
@@ -252,21 +300,29 @@ type RevisionUpdateOne struct {
 	mutation *RevisionMutation
 }
 
-// SetDescription sets the "description" field.
-func (ruo *RevisionUpdateOne) SetDescription(s string) *RevisionUpdateOne {
-	ruo.mutation.SetDescription(s)
+// SetApplied sets the "applied" field.
+func (ruo *RevisionUpdateOne) SetApplied(i int) *RevisionUpdateOne {
+	ruo.mutation.ResetApplied()
+	ruo.mutation.SetApplied(i)
 	return ruo
 }
 
-// SetExecutionState sets the "execution_state" field.
-func (ruo *RevisionUpdateOne) SetExecutionState(rs revision.ExecutionState) *RevisionUpdateOne {
-	ruo.mutation.SetExecutionState(rs)
+// AddApplied adds i to the "applied" field.
+func (ruo *RevisionUpdateOne) AddApplied(i int) *RevisionUpdateOne {
+	ruo.mutation.AddApplied(i)
 	return ruo
 }
 
-// SetExecutedAt sets the "executed_at" field.
-func (ruo *RevisionUpdateOne) SetExecutedAt(t time.Time) *RevisionUpdateOne {
-	ruo.mutation.SetExecutedAt(t)
+// SetTotal sets the "total" field.
+func (ruo *RevisionUpdateOne) SetTotal(i int) *RevisionUpdateOne {
+	ruo.mutation.ResetTotal()
+	ruo.mutation.SetTotal(i)
+	return ruo
+}
+
+// AddTotal adds i to the "total" field.
+func (ruo *RevisionUpdateOne) AddTotal(i int) *RevisionUpdateOne {
+	ruo.mutation.AddTotal(i)
 	return ruo
 }
 
@@ -283,6 +339,26 @@ func (ruo *RevisionUpdateOne) AddExecutionTime(t time.Duration) *RevisionUpdateO
 	return ruo
 }
 
+// SetError sets the "error" field.
+func (ruo *RevisionUpdateOne) SetError(s string) *RevisionUpdateOne {
+	ruo.mutation.SetError(s)
+	return ruo
+}
+
+// SetNillableError sets the "error" field if the given value is not nil.
+func (ruo *RevisionUpdateOne) SetNillableError(s *string) *RevisionUpdateOne {
+	if s != nil {
+		ruo.SetError(*s)
+	}
+	return ruo
+}
+
+// ClearError clears the value of the "error" field.
+func (ruo *RevisionUpdateOne) ClearError() *RevisionUpdateOne {
+	ruo.mutation.ClearError()
+	return ruo
+}
+
 // SetHash sets the "hash" field.
 func (ruo *RevisionUpdateOne) SetHash(s string) *RevisionUpdateOne {
 	ruo.mutation.SetHash(s)
@@ -344,9 +420,15 @@ func (ruo *RevisionUpdateOne) Save(ctx context.Context) (*Revision, error) {
 			}
 			mut = ruo.hooks[i](mut)
 		}
-		if _, err := mut.Mutate(ctx, ruo.mutation); err != nil {
+		v, err := mut.Mutate(ctx, ruo.mutation)
+		if err != nil {
 			return nil, err
 		}
+		nv, ok := v.(*Revision)
+		if !ok {
+			return nil, fmt.Errorf("unexpected node type %T returned from RevisionMutation", v)
+		}
+		node = nv
 	}
 	return node, err
 }
@@ -375,9 +457,9 @@ func (ruo *RevisionUpdateOne) ExecX(ctx context.Context) {
 
 // check runs all checks and user-defined validators on the builder.
 func (ruo *RevisionUpdateOne) check() error {
-	if v, ok := ruo.mutation.ExecutionState(); ok {
-		if err := revision.ExecutionStateValidator(v); err != nil {
-			return &ValidationError{Name: "execution_state", err: fmt.Errorf(`ent: validator failed for field "Revision.execution_state": %w`, err)}
+	if v, ok := ruo.mutation.Total(); ok {
+		if err := revision.TotalValidator(v); err != nil {
+			return &ValidationError{Name: "total", err: fmt.Errorf(`ent: validator failed for field "Revision.total": %w`, err)}
 		}
 	}
 	return nil
@@ -418,25 +500,32 @@ func (ruo *RevisionUpdateOne) sqlSave(ctx context.Context) (_node *Revision, err
 			}
 		}
 	}
-	if value, ok := ruo.mutation.Description(); ok {
+	if value, ok := ruo.mutation.Applied(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
-			Type:   field.TypeString,
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldDescription,
+			Column: revision.FieldApplied,
 		})
 	}
-	if value, ok := ruo.mutation.ExecutionState(); ok {
-		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
-			Type:   field.TypeEnum,
+	if value, ok := ruo.mutation.AddedApplied(); ok {
+		_spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldExecutionState,
+			Column: revision.FieldApplied,
 		})
 	}
-	if value, ok := ruo.mutation.ExecutedAt(); ok {
+	if value, ok := ruo.mutation.Total(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
-			Type:   field.TypeTime,
+			Type:   field.TypeInt,
 			Value:  value,
-			Column: revision.FieldExecutedAt,
+			Column: revision.FieldTotal,
+		})
+	}
+	if value, ok := ruo.mutation.AddedTotal(); ok {
+		_spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{
+			Type:   field.TypeInt,
+			Value:  value,
+			Column: revision.FieldTotal,
 		})
 	}
 	if value, ok := ruo.mutation.ExecutionTime(); ok {
@@ -453,6 +542,19 @@ func (ruo *RevisionUpdateOne) sqlSave(ctx context.Context) (_node *Revision, err
 			Column: revision.FieldExecutionTime,
 		})
 	}
+	if value, ok := ruo.mutation.Error(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Value:  value,
+			Column: revision.FieldError,
+		})
+	}
+	if ruo.mutation.ErrorCleared() {
+		_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
+			Type:   field.TypeString,
+			Column: revision.FieldError,
+		})
+	}
 	if value, ok := ruo.mutation.Hash(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -483,7 +585,7 @@ func (ruo *RevisionUpdateOne) sqlSave(ctx context.Context) (_node *Revision, err
 		if _, ok := err.(*sqlgraph.NotFoundError); ok {
 			err = &NotFoundError{revision.Label}
 		} else if sqlgraph.IsConstraintError(err) {
-			err = &ConstraintError{err.Error(), err}
+			err = &ConstraintError{msg: err.Error(), wrap: err}
 		}
 		return nil, err
 	}
diff --git a/cmd/atlas/internal/migrate/ent/runtime.go b/cmd/atlas/internal/migrate/ent/runtime.go
index dc9d541e..68fb39fa 100644
--- a/cmd/atlas/internal/migrate/ent/runtime.go
+++ b/cmd/atlas/internal/migrate/ent/runtime.go
@@ -6,8 +6,19 @@
 
 package ent
 
+import (
+	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
+	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/schema"
+)
+
 // The init function reads all schema descriptors with runtime code
 // (default values, validators, hooks and policies) and stitches it
 // to their package variables.
 func init() {
+	revisionFields := schema.Revision{}.Fields()
+	_ = revisionFields
+	// revisionDescTotal is the schema descriptor for total field.
+	revisionDescTotal := revisionFields[3].Descriptor()
+	// revision.TotalValidator is a validator for the "total" field. It is called by the builders before save.
+	revision.TotalValidator = revisionDescTotal.Validators[0].(func(int) error)
 }
diff --git a/cmd/atlas/internal/migrate/ent/runtime/runtime.go b/cmd/atlas/internal/migrate/ent/runtime/runtime.go
index 4a0e8864..25005f53 100644
--- a/cmd/atlas/internal/migrate/ent/runtime/runtime.go
+++ b/cmd/atlas/internal/migrate/ent/runtime/runtime.go
@@ -9,6 +9,6 @@ package runtime
 // The schema-stitching logic is generated in ariga.io/atlas/cmd/atlas/internal/migrate/ent/runtime.go
 
 const (
-	Version = "v0.10.2-0.20220502113020-4ac82f5bb3f0"           // Version of ent codegen.
-	Sum     = "h1:qHA4+ANAzDj6BcDLxNgZuzKxFre/RI9r5wwsI2O+1M4=" // Sum of ent codegen.
+	Version = "v0.11.1"                                         // Version of ent codegen.
+	Sum     = "h1:im67R+2W3Nee2bNS2YnoYz8oAF0Qz4AOlIvKRIAEISY=" // Sum of ent codegen.
 )
diff --git a/cmd/atlas/internal/migrate/ent/schema/revision.go b/cmd/atlas/internal/migrate/ent/schema/revision.go
index a6ef6544..a5b5bb0b 100644
--- a/cmd/atlas/internal/migrate/ent/schema/revision.go
+++ b/cmd/atlas/internal/migrate/ent/schema/revision.go
@@ -22,13 +22,19 @@ type Revision struct {
 func (Revision) Fields() []ent.Field {
 	return []ent.Field{
 		field.String("id").
-			StorageKey("version"),
-		field.String("description"),
-		field.Enum("execution_state").
-			Values("ongoing", "ok", "error"),
-		field.Time("executed_at"),
+			StorageKey("version").
+			Immutable(),
+		field.String("description").
+			Immutable(),
+		field.Int("applied"),
+		field.Int("total").
+			Positive(),
+		field.Time("executed_at").
+			Immutable(),
 		field.Int64("execution_time").
 			GoType(time.Duration(0)),
+		field.String("error").
+			Optional(),
 		field.String("hash"),
 		field.String("operator_version"),
 		field.JSON("meta", make(map[string]string)),
diff --git a/cmd/atlas/internal/migrate/migrate.go b/cmd/atlas/internal/migrate/migrate.go
index 60753749..1f310e85 100644
--- a/cmd/atlas/internal/migrate/migrate.go
+++ b/cmd/atlas/internal/migrate/migrate.go
@@ -91,6 +91,20 @@ func (r *EntRevisions) Init(ctx context.Context) error {
 	return r.ec.Schema.Create(ctx, entschema.WithAtlas(true))
 }
 
+// ReadRevision reads a revision from the revisions table.
+//
+// ReadRevision will not return results only saved in cache.
+func (r *EntRevisions) ReadRevision(ctx context.Context, v string) (*migrate.Revision, error) {
+	rev, err := r.ec.Revision.Get(ctx, v)
+	if err != nil && !ent.IsNotFound(err) {
+		return nil, err
+	}
+	if ent.IsNotFound(err) {
+		return nil, migrate.ErrNotExist
+	}
+	return fromEnt(rev), nil
+}
+
 // ReadRevisions reads the revisions from the revisions table.
 //
 // ReadRevisions will not return results only saved to cache.
@@ -100,17 +114,8 @@ func (r *EntRevisions) ReadRevisions(ctx context.Context) (migrate.Revisions, er
 		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,
-		}
+	for i, rev := range revs {
+		ret[i] = fromEnt(rev)
 	}
 	return ret, nil
 }
@@ -146,9 +151,11 @@ func (r *EntRevisions) write(ctx context.Context, rev *migrate.Revision) error {
 	return r.ec.Revision.Create().
 		SetID(rev.Version).
 		SetDescription(rev.Description).
-		SetExecutionState(revision.ExecutionState(rev.ExecutionState)).
+		SetApplied(rev.Applied).
+		SetTotal(rev.Total).
 		SetExecutedAt(rev.ExecutedAt).
 		SetExecutionTime(rev.ExecutionTime).
+		SetError(rev.Error).
 		SetHash(rev.Hash).
 		SetOperatorVersion(rev.OperatorVersion).
 		SetMeta(rev.Meta).
@@ -157,6 +164,21 @@ func (r *EntRevisions) write(ctx context.Context, rev *migrate.Revision) error {
 		Exec(ctx)
 }
 
+func fromEnt(r *ent.Revision) *migrate.Revision {
+	return &migrate.Revision{
+		Version:         r.ID,
+		Description:     r.Description,
+		Applied:         r.Applied,
+		Total:           r.Total,
+		ExecutedAt:      r.ExecutedAt,
+		ExecutionTime:   r.ExecutionTime,
+		Error:           r.Error,
+		Hash:            r.Hash,
+		OperatorVersion: r.OperatorVersion,
+		Meta:            r.Meta,
+	}
+}
+
 func (r *EntRevisions) useCache() bool {
 	// For SQLite dialect and flavors we have to enable the revision write cache to postpone writing to
 	// the database until the transaction wrapping the migration execution has been committed.
diff --git a/go.sum b/go.sum
index e721ca2f..669e9ec5 100644
--- a/go.sum
+++ b/go.sum
@@ -3,7 +3,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
 github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
-github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
 github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
@@ -51,16 +50,13 @@ github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUA
 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -70,9 +66,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index ba97011b..38a5e3f2 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -7,20 +7,20 @@ replace ariga.io/atlas => ../../
 replace ariga.io/atlas/cmd/atlas => ../../cmd/atlas
 
 require (
-	ariga.io/atlas v0.5.0
+	ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7
 	entgo.io/ent v0.11.1
 	github.com/go-sql-driver/mysql v1.6.0
-	github.com/hashicorp/hcl/v2 v2.10.1
-	github.com/lib/pq v1.10.5
-	github.com/mattn/go-sqlite3 v1.14.13
+	github.com/hashicorp/hcl/v2 v2.13.0
+	github.com/lib/pq v1.10.6
+	github.com/mattn/go-sqlite3 v1.14.14
 	github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
 	github.com/rogpeppe/go-internal v1.8.2-0.20220112175052-f3cb5c2c6412
-	github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
+	github.com/stretchr/testify v1.8.0
 	github.com/zclconf/go-cty v1.10.0 // indirect
 )
 
 require (
-	ariga.io/atlas/cmd/atlas v0.0.0-00010101000000-000000000000 // indirect
+	ariga.io/atlas/cmd/atlas v0.5.0 // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
@@ -32,7 +32,7 @@ require (
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/kr/pretty v0.2.1 // indirect
 	github.com/manifoldco/promptui v0.9.0 // indirect
-	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/mattn/go-colorable v0.1.9 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
@@ -45,5 +45,5 @@ require (
 	golang.org/x/text v0.3.7 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/errgo.v2 v2.1.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index a2ffc662..1d69e5fc 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -1,13 +1,8 @@
-entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0 h1:qHA4+ANAzDj6BcDLxNgZuzKxFre/RI9r5wwsI2O+1M4=
-entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0/go.mod h1:Zh61BPvB+cL6VWEyN8f1YoDacrMjQf2KDlDeX26xq2k=
 entgo.io/ent v0.11.1 h1:im67R+2W3Nee2bNS2YnoYz8oAF0Qz4AOlIvKRIAEISY=
 entgo.io/ent v0.11.1/go.mod h1:X5b1YfMayrRTgKGO//8IqpL7XJx0uqdeReEkxNpXROA=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
-github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
 github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
-github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
-github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -25,8 +20,6 @@ github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12f
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
-github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
-github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -34,8 +27,8 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/hashicorp/hcl/v2 v2.10.1 h1:h4Xx4fsrRE26ohAk/1iGF/JBqRQbyUqu5Lvj60U54ys=
-github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
+github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
+github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -45,22 +38,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
-github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
-github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
-github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
+github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
+github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 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-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
-github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
@@ -70,45 +60,32 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
-github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
-github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
 github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
-github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
-github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
 github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
-github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210927094055-39ccf1dd6fa6/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -119,7 +96,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -132,5 +108,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 7c50a90f..f7d3caae 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -179,7 +179,11 @@ type (
 	//
 	// Atlas drivers provide a sql based implementation of this interface by default.
 	RevisionReadWriter interface {
+		// ReadRevisions returns all revisions.
 		ReadRevisions(context.Context) (Revisions, error)
+		// ReadRevision returns a revision by version. Returns ErrNotExist if the version does not exist.
+		ReadRevision(context.Context, string) (*Revision, error)
+		// WriteRevision saves the revision to the storage.
 		WriteRevision(context.Context, *Revision) error
 	}
 
@@ -202,8 +206,10 @@ type (
 		Version string
 		// Description of this migration.
 		Description string
-		// ExecutionState of this migration. One of ["ongoing", "ok", "error"].
-		ExecutionState string
+		// 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.
@@ -318,54 +324,49 @@ func (p *Planner) WritePlan(plan *Plan) error {
 	return nil
 }
 
-const (
-	// StateOngoing is set once a migration file has been started to be applied.
-	StateOngoing = "ongoing"
-	// StateOK is set once a migration file is applied without errors.
-	StateOK = "ok"
-	// StateError  is set once a migration file could not be applied due to an error.
-	StateError = "error"
-)
-
 var (
-	// ErrNoPendingFiles is returned when there are no pending migration files to execute on the managed database.
+	// ErrNoPendingFiles is returned if there are no pending migration files to execute on the managed database.
 	ErrNoPendingFiles = errors.New("sql/migrate: execute: nothing to do")
-	// ErrLockUnsupported is returned when the given driver does not implement the schema.Locker interface.
+	// ErrLockUnsupported is returned if there is no schema.Locker given.
 	ErrLockUnsupported = errors.New("sql/migrate: driver does not support locking")
-	// ErrSnapshotUnsupported is returned when the given driver does not implement the Snapshoter interface.
+	// ErrSnapshotUnsupported is returned if there is no Snapshoter given.
 	ErrSnapshotUnsupported = errors.New("sql/migrate: driver does not support taking a database snapshot")
+	// ErrNotExist is returned if the requested revision is not found in the storage.
+	ErrNotExist = errors.New("sql/migrate: revision not found")
+	// ErrScanUnsupported is returned if there is no Scanner given.
+	ErrScanUnsupported = errors.New("sql/migrate: revision not found")
 )
 
 // NewExecutor creates a new Executor with default values. // TODO(masseelch): Operator Version and other Meta
 func NewExecutor(drv Driver, dir Dir, rrw RevisionReadWriter, opts ...ExecutorOption) (*Executor, error) {
 	if drv == nil {
-		return nil, errors.New("sql/migrate: execute: drv cannot be nil")
+		return nil, errors.New("sql/migrate: execute: no driver given")
 	}
 	if dir == nil {
-		return nil, errors.New("sql/migrate: execute: dir cannot be nil")
+		return nil, errors.New("sql/migrate: execute: no dir given")
 	}
 	if rrw == nil {
-		return nil, errors.New("sql/migrate: execute: rrw cannot be nil")
+		return nil, errors.New("sql/migrate: execute: no revision storage given")
+	}
+	ex := &Executor{drv: drv, dir: dir, rrw: rrw}
+	for _, opt := range opts {
+		if err := opt(ex); err != nil {
+			return nil, err
+		}
+	}
+	if ex.log == nil {
+		ex.log = NopLogger{}
 	}
-	// If the driver does not support acquiring a lock, don't execute migrations as this can potentially be fatal.
 	if _, ok := drv.(schema.Locker); !ok {
 		return nil, ErrLockUnsupported
 	}
-	// Check if the driver implements Snapshoter interface.
 	if _, ok := drv.(Snapshoter); !ok {
 		return nil, ErrSnapshotUnsupported
 	}
-	// Check if the Dir implements Scanner interface.
 	if _, ok := dir.(Scanner); !ok {
-		return nil, errors.New("sql/migrate: execute: no scanner available")
+		return nil, ErrScanUnsupported
 	}
-	p := &Executor{drv: drv, dir: dir, rrw: rrw}
-	for _, opt := range opts {
-		if err := opt(p); err != nil {
-			return nil, err
-		}
-	}
-	return p, nil
+	return ex, nil
 }
 
 // WithLogger sets the Logger of an Executor.
@@ -386,19 +387,7 @@ func (e *Executor) Lock(ctx context.Context) (schema.UnlockFunc, error) {
 	return unlock, nil
 }
 
-// DirtyError is returned if the revision table is dirty.
-type DirtyError struct {
-	Version string
-	State   string
-}
-
-// Error implements the error interface.
-func (e DirtyError) Error() string {
-	return fmt.Sprintf("dirty migration state: version %q has state %q", e.Version, e.State)
-}
-
-// Pending returns all pending (not applied) migration files in the migration directory. It will return an error, if
-// there is at least one revision in a "not-ok" state (error or ongoing).
+// Pending returns all pending (not fully applied) migration files in the migration directory.
 func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 	// Don't operate with a broken migration directory.
 	if err := Validate(e.dir); err != nil {
@@ -409,15 +398,8 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 	if err != nil {
 		return nil, fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
 	}
-	// Check for all revisions to be "okay".
-	for _, r := range revs {
-		if r.ExecutionState != StateOK {
-			return nil, fmt.Errorf("sql/migrate: execute: %w", DirtyError{r.Version, r.ExecutionState})
-		}
-	}
 	// Select the correct migration files.
-	sc := e.dir.(Scanner)
-	migrations, err := sc.Files()
+	migrations, err := e.dir.(Scanner).Files()
 	if err != nil {
 		return nil, fmt.Errorf("sql/migrate: execute: select migration files: %w", err)
 	}
@@ -436,79 +418,115 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 	}
 	for i := range revs {
 		m := migrations[i]
-		v, err := sc.Version(m)
+		v, err := e.dir.(Scanner).Version(m)
 		if err != nil {
 			return nil, fmt.Errorf("sql/migrate: execute: scan version from %q: %w", m.Name(), err)
 		}
-		d, err := sc.Desc(m)
+		d, err := e.dir.(Scanner).Desc(m)
 		if err != nil {
 			return nil, fmt.Errorf("sql/migrate: execute: scan description from %q: %w", m.Name(), err)
 		}
 		r := revs[i]
-		if v != r.Version || d != r.Description || hf[i].H != r.Hash {
+		// All fully applied revisions must match both version and description.
+		// In case of a not fully applied migration, the hash might change since the file could have been edited to fix
+		// a previous failure. Thus, hashes don't have to match in that case.
+		hash := (i < len(revs)-1 && hf[i].H != r.Hash) || (i == len(revs)-1 && r.Complete() == nil && hf[i].H != r.Hash)
+		if v != r.Version || d != r.Description || hash {
 			return nil, fmt.Errorf("sql/migrate: execute: revisions and migrations mismatch: rev %q <> file %q", v, r.Version)
 		}
 	}
-	if len(migrations) == len(revs) {
+	// If there are no revisions yet, return all migration files as pending.
+	if len(revs) == 0 {
+		return migrations, nil
+	}
+	pending := migrations[len(revs):]
+	// If the last revision is not applied in full, it is considered pending as well.
+	if last := revs[len(revs)-1]; last.Applied < last.Total {
+		pending = append(migrations[len(revs)-1:len(revs)], pending...)
+	}
+	if len(pending) == 0 {
 		return nil, ErrNoPendingFiles
 	}
-	return migrations[len(revs):], nil
+	return pending, nil
 }
 
 // Execute executes the given migration file on the database. It does not check for the database to be clean before
 // attempting to apply the changes. This behavior is required to enabled "fixing" a broken state.
 func (e *Executor) Execute(ctx context.Context, m File) (err error) {
-	r := &Revision{ExecutedAt: time.Now(), ExecutionState: StateOngoing}
-	// Make sure to store the Revision information.
-	defer func(ctx context.Context, rrw RevisionReadWriter, r *Revision) {
-		if err2 := e.rrw.WriteRevision(ctx, r); err2 != nil {
-			err = wrap(fmt.Errorf("execute: write revision: %w", err2), err)
-		}
-	}(ctx, e.rrw, r)
-	sc := e.dir.(Scanner)
-	r.Version, err = sc.Version(m)
+	hf, err := HashSum(e.dir)
 	if err != nil {
-		return r.setGoErr(fmt.Errorf("sql/migrate: execute: scan version from %q: %w", m.Name(), err))
+		return fmt.Errorf("sql/migrate: execute: compute hash: %w", err)
 	}
-	r.Description, err = sc.Desc(m)
+	version, err := e.dir.(Scanner).Version(m)
 	if err != nil {
-		return r.setGoErr(fmt.Errorf("sql/migrate: execute: scan description from %q: %w", m.Name(), err))
-
-	}
-	if e.log != nil {
-		e.log.Log(LogFile{r.Version, r.Description})
+		return fmt.Errorf("sql/migrate: execute: scan version from %q: %w", m.Name(), err)
 	}
-	hf, err := HashSum(e.dir)
+	desc, err := e.dir.(Scanner).Desc(m)
 	if err != nil {
-		return fmt.Errorf("sql/migrate: execute: create hash file: %w", err)
+		return fmt.Errorf("sql/migrate: execute: scan description from %q: %w", m.Name(), err)
 	}
-	r.Hash, err = hf.sumByName(m.Name())
+	hash, err := hf.sumByName(m.Name())
 	if err != nil {
-		return r.setGoErr(fmt.Errorf("sql/migrate: execute: scanning checksum for file %q: %w", m.Name(), err))
+		return fmt.Errorf("sql/migrate: execute: scanning checksum from %q: %w", m.Name(), err)
 	}
-	stmts, err := sc.Stmts(m)
+	stmts, err := e.dir.(Scanner).Stmts(m)
 	if err != nil {
-		return r.setGoErr(fmt.Errorf("sql/migrate: execute: scanning statements from file %q: %w", m.Name(), err))
+		return fmt.Errorf("sql/migrate: execute: scanning statements from %q: %w", m.Name(), err)
+	}
+	// If there already is a revision with this version in the database,
+	// and it is partially applied, continue where the last attempt was left off.
+	r, err := e.rrw.ReadRevision(ctx, version)
+	if err != nil && !errors.Is(err, ErrNotExist) {
+		return fmt.Errorf("sql/migrate: execute: read revision: %w", err)
+	}
+	if errors.Is(err, ErrNotExist) {
+		// Haven't seen this file before, create a new revision.
+		r = &Revision{
+			Version:     version,
+			Description: desc,
+			Total:       len(stmts),
+			ExecutedAt:  time.Now(),
+			Hash:        hash,
+		}
 	}
 	// Save once to mark as started in the database.
-	if err := e.rrw.WriteRevision(ctx, r); err != nil {
-		return fmt.Errorf("sql/migrate: execute: write revision: %w", err)
+	if err = writeRevision(ctx, e.rrw, r); err != nil {
+		return err
 	}
-	for _, stmt := range stmts {
-		if e.log != nil {
-			e.log.Log(LogStmt{stmt})
+	// Make sure to store the Revision information.
+	defer func(ctx context.Context, rrw RevisionReadWriter, r *Revision) {
+		if err2 := writeRevision(ctx, rrw, r); err2 != nil {
+			err = wrap(err2, err)
 		}
-		if _, err := e.drv.ExecContext(ctx, stmt); err != nil {
-			return r.setSQLErr(
-				fmt.Errorf("sql/migrate: execute: executing statement %q from version %q: %w", stmt, r.Version, err),
-				stmt,
-			)
+	}(ctx, e.rrw, r)
+	e.log.Log(LogFile{r.Version, r.Description, r.Applied})
+	for i, stmt := range stmts {
+		if i < r.Applied {
+			// Skip if already applied.
+			continue
+		}
+		e.log.Log(LogStmt{stmt})
+		if _, err = e.drv.ExecContext(ctx, stmt); err != nil {
+			e.log.Log(LogError{Error: err})
+			r.setSQLErr(stmt, err)
+			return fmt.Errorf("sql/migrate: execute: executing statement %q from version %q: %w", stmt, r.Version, err)
+		}
+		r.Applied++
+		if err = writeRevision(ctx, e.rrw, r); err != nil {
+			return err
 		}
 	}
-	r.done(true)
+	r.done()
 	return
 }
 
+func writeRevision(ctx context.Context, w RevisionReadWriter, r *Revision) error {
+	if err := w.WriteRevision(ctx, r); err != nil {
+		return fmt.Errorf("sql/migrate: execute: write revision: %w", err)
+	}
+	return nil
+}
+
 // ExecuteN executes n pending migration files. If n<=0 all pending migration files are executed. It will not attempt
 // an execution if the database is not "clean" (has only successfully applied migrations).
 func (e *Executor) ExecuteN(ctx context.Context, n int) error {
@@ -527,34 +545,22 @@ func (e *Executor) ExecuteN(ctx context.Context, n int) error {
 		}
 		pending = pending[:n]
 	}
-	if e.log != nil {
-		names := make([]string, len(pending))
-		for i := range pending {
-			names[i] = pending[i].Name()
-		}
-		revs, err := e.rrw.ReadRevisions(ctx)
-		if err != nil {
-			return fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
-		}
-		last := pending[len(pending)-1]
-		v, err := e.dir.(Scanner).Version(last)
-		if err != nil {
-			return fmt.Errorf("sql/migrate: execute: scan version from %q: %w", last.Name(), err)
-		}
-		l := LogExecution{To: v, Files: names}
-		if len(revs) > 0 {
-			l.From = revs[len(revs)-1].Version
-		}
-		e.log.Log(l)
+	revs, err := e.rrw.ReadRevisions(ctx)
+	if err != nil {
+		return fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
+	}
+	if err := LogIntro(struct {
+		Logger
+		Scanner
+	}{e.log, e.dir.(Scanner)}, revs, pending); err != nil {
+		return err
 	}
 	for _, m := range pending {
 		if err := e.Execute(ctx, m); err != nil {
 			return err
 		}
 	}
-	if e.log != nil {
-		e.log.Log(LogDone{})
-	}
+	e.log.Log(LogDone{})
 	return err
 }
 
@@ -618,6 +624,11 @@ func (NopRevisionReadWriter) ReadRevisions(context.Context) (Revisions, error) {
 	return nil, nil
 }
 
+// ReadRevision implements RevisionsReadWriter.ReadRevision,
+func (NopRevisionReadWriter) ReadRevision(context.Context, string) (*Revision, error) {
+	return nil, ErrNotExist
+}
+
 // WriteRevision implements RevisionsReadWriter.WriteRevision,
 func (NopRevisionReadWriter) WriteRevision(context.Context, *Revision) error {
 	return nil
@@ -625,26 +636,28 @@ func (NopRevisionReadWriter) WriteRevision(context.Context, *Revision) error {
 
 var _ RevisionReadWriter = (*NopRevisionReadWriter)(nil)
 
+// Complete returns if a revision has been applied without errors and in full.
+func (r *Revision) Complete() error {
+	if r.Error != "" {
+		return errors.New(r.Error)
+	}
+	return nil
+}
+
 // done computes and sets the ExecutionTime.
-func (r *Revision) done(ok bool) {
+func (r *Revision) done() {
 	r.ExecutionTime = time.Now().Sub(r.ExecutedAt)
-	if ok {
-		r.ExecutionState = StateOK
-	} else {
-		r.ExecutionState = StateError
-	}
 }
 
 func (r *Revision) setGoErr(err error) error {
-	r.done(false)
+	r.done()
 	r.Error = fmt.Sprintf("Go:\n%s", err)
 	return err
 }
 
-func (r *Revision) setSQLErr(err error, stmt string) error {
-	r.done(false)
+func (r *Revision) setSQLErr(stmt string, err error) {
+	r.done()
 	r.Error = fmt.Sprintf("Statement:\n%s\n\nError:\n%s", stmt, err)
-	return err
 }
 
 type (
@@ -675,6 +688,9 @@ type (
 		Version string
 		// Desc of migration executed.
 		Desc string
+		// Skip holds the number of stmts of this file that will be skipped.
+		// This happens, if a migration file was only applied partially and will now continue to be applied.
+		Skip int
 	}
 
 	// LogStmt is sent if a new SQL statement is executed.
@@ -684,12 +700,51 @@ type (
 
 	// LogDone is sent if the execution is done.
 	LogDone struct{}
+
+	// LogError is sent if there is an error while execution.
+	LogError struct {
+		Error error
+	}
+
+	// NopLogger is a Logger that does nothing.
+	// It is useful for one-time replay of the migration directory.
+	NopLogger struct{}
 )
 
 func (LogExecution) logEntry() {}
 func (LogFile) logEntry()      {}
 func (LogStmt) logEntry()      {}
 func (LogDone) logEntry()      {}
+func (LogError) logEntry()     {}
+
+// Log implements the Logger interface.
+func (NopLogger) Log(LogEntry) {}
+
+// LogScanner stitches Logger and Scanner into one interface.
+type LogScanner interface {
+	Logger
+	Scanner
+}
+
+// LogIntro gathers some meta information from the migration files and stored revisions to
+// log some general information prior to actual execution.
+func LogIntro(l LogScanner, revs Revisions, files []File) error {
+	names := make([]string, len(files))
+	for i := range files {
+		names[i] = files[i].Name()
+	}
+	last := files[len(files)-1]
+	v, err := l.(Scanner).Version(last)
+	if err != nil {
+		return fmt.Errorf("sql/migrate: execute: scan version from %q: %w", last.Name(), err)
+	}
+	e := LogExecution{To: v, Files: names}
+	if len(revs) > 0 {
+		e.From = revs[len(revs)-1].Version
+	}
+	l.Log(e)
+	return nil
+}
 
 // LocalDir implements Dir for a local path. It implements the Scanner interface compatible with
 // migration files generated by the DefaultFormatter.


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index 14c8ce5b..b074a6f5 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -12,6 +12,7 @@ import (
 	"io/ioutil"
 	"net/url"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -38,13 +39,13 @@ func TestMigrate_Apply(t *testing.T) {
 	p := t.TempDir()
 
 	// Fails on empty directory.
-	b, err := runCmd(
+	s, err := runCmd(
 		Root, "migrate", "apply",
 		"--dir", "file://"+p,
 		"-u", openSQLite(t, ""),
 	)
 	require.NoError(t, err)
-	require.Equal(t, "The migration directory is synced with the database, no migration files to execute\n", b)
+	require.Equal(t, "The migration directory is synced with the database, no migration files to execute\n", s)
 
 	// Fails on directory without sum file.
 	require.NoError(t, os.Rename(
@@ -89,7 +90,7 @@ func TestMigrate_Apply(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, f.Close())
 
-	s, err := runCmd(
+	s, err = runCmd(
 		Root, "migrate", "apply",
 		"--dir", "file://testdata/sqlite",
 		"--url", fmt.Sprintf("sqlitelockapply://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
@@ -109,7 +110,94 @@ func TestMigrate_Apply(t *testing.T) {
 	require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);")   // logs statement
 	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // does not execute second file
 	require.Contains(t, s, "1 migrations")                             // logs amount of migrations
-	require.Contains(t, s, "1 sql statements")                         // logs amount of statement
+	require.Contains(t, s, "1 sql statements")
+
+	// Transactions will be wrapped per file. If the second file has an error, first still is applied.
+	s, err = runCmd(
+		Root, "migrate", "apply",
+		"--dir", "file://testdata/sqlite2",
+		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
+	)
+	require.Error(t, err)
+	require.Contains(t, s, "20220318104614")                           // log to version
+	require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);")   // logs statement
+	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;")    // does execute first stmt first second file
+	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;")    // does execute second stmt first second file
+	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // but not third
+	require.Contains(t, s, "1 migrations ok (1 with errors)")          // logs amount of migrations
+	require.Contains(t, s, "2 sql statements ok (1 with errors)")      // logs amount of statement
+	require.Contains(t, s, "Error: Execution had errors:")             // logs error summary
+	require.Contains(t, s, "near \"asdasd\": syntax error")            // logs error summary
+
+	c, err := sqlclient.Open(context.Background(), fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")))
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		require.NoError(t, c.Close())
+	})
+	sch, err := c.InspectSchema(context.Background(), "", nil)
+	tbl, ok := sch.Table("tbl")
+	require.True(t, ok)
+	_, ok = tbl.Column("col_2")
+	require.False(t, ok)
+	_, ok = tbl.Column("col_3")
+	require.False(t, ok)
+	rrw, err := migrate2.NewEntRevisions(c)
+	require.NoError(t, err)
+	require.NoError(t, rrw.Init(context.Background()))
+	revs, err := rrw.ReadRevisions(context.Background())
+	require.NoError(t, err)
+	require.Len(t, revs, 2)
+	require.Equal(t, 1, revs[1].Applied)
+	require.Equal(t, 3, revs[1].Total)
+
+	// Running again will pick up the failed statement and try it again.
+	s, err = runCmd(
+		Root, "migrate", "apply",
+		"--dir", "file://testdata/sqlite2",
+		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
+	)
+	require.Error(t, err)
+	require.NotContains(t, s, "20220318104614")                         // first version is applied already
+	require.Contains(t, s, "20220318104615")                            // retry second (partially applied)
+	require.NotContains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // will not attempt stmts from first file
+	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;")  // does not execute first stmt first second file
+	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;")     // does execute second stmt first second file
+	require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;")  // but not third
+	require.Contains(t, s, "0 migrations ok (1 with errors)")           // logs amount of migrations
+	require.Contains(t, s, "0 sql statements ok (1 with errors)")       // logs amount of statement
+	require.Contains(t, s, "Error: Execution had errors:")              // logs error summary
+	require.Contains(t, s, "near \"asdasd\": syntax error")             // logs error summary
+
+	// Fixing the migration file will finish without errors.
+	require.NoError(t, exec.Command("cp", "-r", "testdata/sqlite2", "testdata/sqlite3").Run())
+	t.Cleanup(func() {
+		require.NoError(t, os.RemoveAll("testdata/sqlite3"))
+	})
+	require.NoError(t, exec.Command("sed", "-i", "s/asdasd //g", "testdata/sqlite3/20220318104615_second.sql").Run())
+	_, err = runCmd(Root, "migrate", "hash", "--force", "--dir", "file://testdata/sqlite3")
+	require.NoError(t, err)
+	s, err = runCmd(
+		Root, "migrate", "apply",
+		"--dir", "file://testdata/sqlite3",
+		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
+	)
+	require.NoError(t, err)
+	require.Contains(t, s, "20220318104615")                        // retry second (partially applied)
+	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;") // does execute second stmt first second file
+	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // does execute second stmt first second file
+	require.Contains(t, s, "1 migrations")                          // logs amount of migrations
+	require.Contains(t, s, "2")                                     // logs amount of statement
+	require.NotContains(t, s, "Error: Execution had errors:")       // logs error summary
+	require.NotContains(t, s, "near \"asdasd\": syntax error")      // logs error summary
+
+	// Running again will report database being in clean state.
+	s, err = runCmd(
+		Root, "migrate", "apply",
+		"--dir", "file://testdata/sqlite3",
+		"--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")),
+	)
+	require.NoError(t, err)
+	require.Equal(t, "The migration directory is synced with the database, no migration files to execute\n", s)
 
 	// Dry run will print the statements in second migration file without executing them. No changes to the revisions
 	// will be done.
@@ -123,20 +211,20 @@ func TestMigrate_Apply(t *testing.T) {
 	require.NoError(t, err)
 	require.Contains(t, s, "20220318104615")                        // log to version
 	require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // logs statement
-	c, err := sqlclient.Open(context.Background(), fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")))
+	c1, err := sqlclient.Open(context.Background(), fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")))
 	require.NoError(t, err)
 	t.Cleanup(func() {
-		require.NoError(t, c.Close())
+		require.NoError(t, c1.Close())
 	})
-	sch, err := c.InspectSchema(context.Background(), "", nil)
-	tbl, ok := sch.Table("tbl")
+	sch, err = c1.InspectSchema(context.Background(), "", nil)
+	tbl, ok = sch.Table("tbl")
 	require.True(t, ok)
 	_, ok = tbl.Column("col_2")
 	require.False(t, ok)
-	rrw, err := migrate2.NewEntRevisions(c)
+	rrw, err = migrate2.NewEntRevisions(c1)
 	require.NoError(t, err)
 	require.NoError(t, rrw.Init(context.Background()))
-	revs, err := rrw.ReadRevisions(context.Background())
+	revs, err = rrw.ReadRevisions(context.Background())
 	require.NoError(t, err)
 	require.Len(t, revs, 1)
 }
@@ -145,6 +233,7 @@ func TestMigrate_Diff(t *testing.T) {
 	p := t.TempDir()
 
 	// Will create migration directory if not existing.
+	MigrateFlags.Force = false
 	_, err := runCmd(
 		Root, "migrate", "diff",
 		"name",
diff --git a/cmd/atlas/internal/cmdapi/testdata/sqlite2/20220318104614_initial.sql b/cmd/atlas/internal/cmdapi/testdata/sqlite2/20220318104614_initial.sql
new file mode 100644
index 00000000..eaf12fab
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/sqlite2/20220318104614_initial.sql
@@ -0,0 +1,2 @@
+-- create "tbl" table
+CREATE TABLE tbl (`col` int NOT NULL);
diff --git a/cmd/atlas/internal/cmdapi/testdata/sqlite2/20220318104615_second.sql b/cmd/atlas/internal/cmdapi/testdata/sqlite2/20220318104615_second.sql
new file mode 100644
index 00000000..05c8b083
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/sqlite2/20220318104615_second.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `tbl` ADD `col_2` bigint;
+asdasd ALTER TABLE `tbl` ADD `col_3` bigint; -- will fail
+ALTER TABLE `tbl` ADD `col_4` bigint;
diff --git a/cmd/atlas/internal/cmdapi/testdata/sqlite2/atlas.sum b/cmd/atlas/internal/cmdapi/testdata/sqlite2/atlas.sum
new file mode 100644
index 00000000..e976551c
--- /dev/null
+++ b/cmd/atlas/internal/cmdapi/testdata/sqlite2/atlas.sum
@@ -0,0 +1,3 @@
+h1:lXdG49p5Vr5b9eARNKq3Gkgd+flbQXDM+XDyB6b2nzw=
+20220318104614_initial.sql h1:FifWjY2X0g2YVnb18Qm+QBPvoldDOOob7bS0LrFuCXc=
+20220318104615_second.sql h1:UA1TOODS2yU138E2HBlChe/O8vSmTRxkHs4OJOUK3K8=
diff --git a/cmd/atlas/internal/migrate/ent/enttest/enttest.go b/cmd/atlas/internal/migrate/ent/enttest/enttest.go
index 2ba12539..cc3b0be0 100644
--- a/cmd/atlas/internal/migrate/ent/enttest/enttest.go
+++ b/cmd/atlas/internal/migrate/ent/enttest/enttest.go
@@ -13,6 +13,7 @@ import (
 	// required by schema hooks.
 	_ "ariga.io/atlas/cmd/atlas/internal/migrate/ent/runtime"
 
+	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/migrate"
 	"entgo.io/ent/dialect/sql/schema"
 )
 
@@ -63,10 +64,7 @@ func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Cl
 		t.Error(err)
 		t.FailNow()
 	}
-	if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
-		t.Error(err)
-		t.FailNow()
-	}
+	migrateSchema(t, c, o)
 	return c
 }
 
@@ -74,9 +72,17 @@ func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Cl
 func NewClient(t TestingT, opts ...Option) *ent.Client {
 	o := newOptions(opts)
 	c := ent.NewClient(o.opts...)
-	if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
+	migrateSchema(t, c, o)
+	return c
+}
+func migrateSchema(t TestingT, c *ent.Client, o *options) {
+	tables, err := schema.CopyTables(migrate.Tables)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil {
 		t.Error(err)
 		t.FailNow()
 	}
-	return c
 }
diff --git a/cmd/atlas/internal/migrate/migrate_test.go b/cmd/atlas/internal/migrate/migrate_test.go
index 73ddeec2..14dffc81 100644
--- a/cmd/atlas/internal/migrate/migrate_test.go
+++ b/cmd/atlas/internal/migrate/migrate_test.go
@@ -50,9 +50,10 @@ func TestEntRevisions_Flush(t *testing.T) {
 
 	// Writing will only fill the cache.
 	require.NoError(t, r.WriteRevision(ctx, &migrate.Revision{
-		Version:        "version",
-		ExecutedAt:     time.Now(),
-		ExecutionState: "ongoing",
+		Version:    "version",
+		ExecutedAt: time.Now(),
+		Applied:    1,
+		Total:      2,
 	}))
 	require.Len(t, r.cache, 1)
 	revs, err := r.ReadRevisions(ctx)
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index e0276eea..741a27d9 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -538,6 +538,15 @@ func (r *rrw) WriteRevision(_ context.Context, rev *migrate.Revision) error {
 	return nil
 }
 
+func (r *rrw) ReadRevision(_ context.Context, v string) (*migrate.Revision, error) {
+	for _, rev := range *r {
+		if rev.Version == v {
+			return rev, nil
+		}
+	}
+	return nil, migrate.ErrNotExist
+}
+
 func (r *rrw) ReadRevisions(context.Context) (migrate.Revisions, error) {
 	return migrate.Revisions(*r), nil
 }
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index 431b2b07..1ff28920 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -9,7 +9,6 @@ import (
 	"database/sql"
 	_ "embed"
 	"errors"
-	"fmt"
 	"io"
 	"io/fs"
 	"os"
@@ -274,9 +273,10 @@ func TestLocalDir(t *testing.T) {
 
 	files, err := d.Files()
 	require.NoError(t, err)
-	require.Len(t, files, 2)
+	require.Len(t, files, 3)
 	require.Equal(t, "1.a_sub.up.sql", files[0].Name())
 	require.Equal(t, "2.10.x-20_description.sql", files[1].Name())
+	require.Equal(t, "3_partly.sql", files[2].Name())
 
 	stmts, err := d.Stmts(files[0])
 	require.NoError(t, err)
@@ -299,20 +299,77 @@ func TestLocalDir(t *testing.T) {
 	require.Equal(t, "description", desc)
 }
 
+func TestExecutor_Pending(t *testing.T) {
+	var (
+		drv  = &lockMockDriver{&mockDriver{}}
+		rrw  = &mockRevisionReadWriter{}
+		log  = &mockLogger{}
+		rev1 = &migrate.Revision{
+			Version:     "1.a",
+			Description: "sub.up",
+			Applied:     2,
+			Total:       2,
+			Hash:        "nXyZR020M/mH7LxkoTkJr7BcQkipVg90imQ9I4595dw=",
+		}
+		rev2 = &migrate.Revision{
+			Version:     "2.10.x-20",
+			Description: "description",
+			Applied:     1,
+			Total:       1,
+			Hash:        "wQB3Vh3PHVXQg9OD3Gn7TBxbZN3r1Qb7TtAE1g3q9mQ=",
+		}
+		rev3 = &migrate.Revision{
+			Version:     "3",
+			Description: "partly",
+			Applied:     1,
+			Total:       2,
+			Error:       "this is an migration error",
+			Hash:        "+O40cAXHgvMClnynHd5wggPAeZAk7zSEaNgzXCZOfmY=",
+		}
+	)
+	dir, err := migrate.NewLocalDir(filepath.Join("testdata/migrate", "sub"))
+	require.NoError(t, err)
+	ex, err := migrate.NewExecutor(drv, dir, rrw, migrate.WithLogger(log))
+	require.NoError(t, err)
+
+	// All are pending
+	p, err := ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 3)
+
+	// 2 are pending.
+	*rrw = mockRevisionReadWriter(migrate.Revisions{rev1})
+	p, err = ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 2)
+
+	// Only the last one is pending (in full).
+	*rrw = mockRevisionReadWriter(migrate.Revisions{rev1, rev2})
+	p, err = ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 1)
+
+	// First statement of last one is marked as applied, second isn't. Third file is still pending.
+	*rrw = mockRevisionReadWriter(migrate.Revisions{rev1, rev2, rev3})
+	p, err = ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 1)
+}
+
 func TestExecutor(t *testing.T) {
 	// Passing nil raises error.
 	ex, err := migrate.NewExecutor(nil, nil, nil)
-	require.EqualError(t, err, "sql/migrate: execute: drv cannot be nil")
+	require.EqualError(t, err, "sql/migrate: execute: no driver given")
 	require.Nil(t, ex)
 
 	ex, err = migrate.NewExecutor(&mockDriver{}, nil, nil)
-	require.EqualError(t, err, "sql/migrate: execute: dir cannot be nil")
+	require.EqualError(t, err, "sql/migrate: execute: no dir given")
 	require.Nil(t, ex)
 
 	dir, err := migrate.NewLocalDir(t.TempDir())
 	require.NoError(t, err)
 	ex, err = migrate.NewExecutor(&mockDriver{}, dir, nil)
-	require.EqualError(t, err, "sql/migrate: execute: rrw cannot be nil")
+	require.EqualError(t, err, "sql/migrate: execute: no revision storage given")
 	require.Nil(t, ex)
 
 	// Does not work if no locking mechanism is provided.
@@ -335,16 +392,18 @@ func TestExecutor(t *testing.T) {
 		rrw  = &mockRevisionReadWriter{}
 		log  = &mockLogger{}
 		rev1 = &migrate.Revision{
-			Version:        "1.a",
-			Description:    "sub.up",
-			ExecutionState: "ok",
-			Hash:           "nXyZR020M/mH7LxkoTkJr7BcQkipVg90imQ9I4595dw=",
+			Version:     "1.a",
+			Description: "sub.up",
+			Applied:     2,
+			Total:       2,
+			Hash:        "nXyZR020M/mH7LxkoTkJr7BcQkipVg90imQ9I4595dw=",
 		}
 		rev2 = &migrate.Revision{
-			Version:        "2.10.x-20",
-			Description:    "description",
-			ExecutionState: "ok",
-			Hash:           "wQB3Vh3PHVXQg9OD3Gn7TBxbZN3r1Qb7TtAE1g3q9mQ=",
+			Version:     "2.10.x-20",
+			Description: "description",
+			Applied:     1,
+			Total:       1,
+			Hash:        "wQB3Vh3PHVXQg9OD3Gn7TBxbZN3r1Qb7TtAE1g3q9mQ=",
 		}
 	)
 	dir, err = migrate.NewLocalDir(filepath.Join("testdata/migrate", "sub"))
@@ -352,8 +411,8 @@ func TestExecutor(t *testing.T) {
 	ex, err = migrate.NewExecutor(drv, dir, rrw, migrate.WithLogger(log))
 	require.NoError(t, err)
 
-	// Applies all of them.
-	require.NoError(t, ex.ExecuteN(context.Background(), 0))
+	// Applies two of them.
+	require.NoError(t, ex.ExecuteN(context.Background(), 2))
 	require.Equal(t, drv.executed, []string{
 		"CREATE TABLE t_sub(c int);", "ALTER TABLE t_sub ADD c1 int;", "ALTER TABLE t_sub ADD c2 int;",
 	})
@@ -371,8 +430,11 @@ func TestExecutor(t *testing.T) {
 	require.Equal(t, drv.unlockCounter, 1)
 	require.True(t, drv.released())
 
-	// No pending files.
-	require.ErrorIs(t, ex.ExecuteN(context.Background(), 0), migrate.ErrNoPendingFiles)
+	// Partly is pending.
+	p, err := ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 1)
+	require.Equal(t, "3_partly.sql", p[0].Name())
 
 	// Apply one by one.
 	*rrw = mockRevisionReadWriter{}
@@ -387,27 +449,36 @@ func TestExecutor(t *testing.T) {
 		"CREATE TABLE t_sub(c int);", "ALTER TABLE t_sub ADD c1 int;", "ALTER TABLE t_sub ADD c2 int;",
 	})
 	requireEqualRevisions(t, migrate.Revisions{rev1, rev2}, migrate.Revisions(*rrw))
-	require.ErrorIs(t, ex.ExecuteN(context.Background(), 0), migrate.ErrNoPendingFiles)
-
-	require.Equal(t, drv.lockCounter, 3)
-	require.Equal(t, drv.unlockCounter, 3)
+	require.Equal(t, 2, drv.lockCounter)
+	require.Equal(t, 2, drv.unlockCounter)
 	require.True(t, drv.released())
 
+	// Partly is pending.
+	p, err = ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 1)
+	require.Equal(t, "3_partly.sql", p[0].Name())
+
 	// Suppose first revision is already executed, only execute second migration file.
 	*rrw = mockRevisionReadWriter(migrate.Revisions{rev1})
 	*drv = lockMockDriver{&mockDriver{}}
 
-	require.NoError(t, ex.ExecuteN(context.Background(), 0))
-	require.Equal(t, drv.executed, []string{"ALTER TABLE t_sub ADD c2 int;"})
+	require.NoError(t, ex.ExecuteN(context.Background(), 1))
+	require.Equal(t, []string{"ALTER TABLE t_sub ADD c2 int;"}, drv.executed)
 	requireEqualRevisions(t, migrate.Revisions{rev1, rev2}, migrate.Revisions(*rrw))
-	require.ErrorIs(t, ex.ExecuteN(context.Background(), 0), migrate.ErrNoPendingFiles)
 
-	require.Equal(t, drv.lockCounter, 2)
-	require.Equal(t, drv.unlockCounter, 2)
+	// Partly is pending.
+	p, err = ex.Pending(context.Background())
+	require.NoError(t, err)
+	require.Len(t, p, 1)
+	require.Equal(t, "3_partly.sql", p[0].Name())
+
+	require.Equal(t, 1, drv.lockCounter)
+	require.Equal(t, 1, drv.unlockCounter)
 	require.True(t, drv.released())
 
 	// Unknown revision present.
-	*rrw = mockRevisionReadWriter(migrate.Revisions{&migrate.Revision{Version: "unknown", ExecutionState: "ok"}})
+	*rrw = mockRevisionReadWriter(migrate.Revisions{&migrate.Revision{Version: "unknown"}})
 	*drv = lockMockDriver{&mockDriver{}}
 	require.EqualError(t, ex.ExecuteN(context.Background(), 0),
 		`sql/migrate: execute: revisions and migrations mismatch: rev "1.a" <> file "unknown"`,
@@ -418,7 +489,7 @@ func TestExecutor(t *testing.T) {
 	require.True(t, drv.released())
 
 	// More revisions than migration files.
-	*rrw = mockRevisionReadWriter(migrate.Revisions{{Version: "unknown", ExecutionState: "ok"}, rev1, rev2})
+	*rrw = mockRevisionReadWriter(migrate.Revisions{{Version: "unknown"}, rev1, rev2, rev2})
 	*drv = lockMockDriver{&mockDriver{}}
 	require.EqualError(t, ex.ExecuteN(context.Background(), 0),
 		`sql/migrate: execute: revisions and migrations mismatch: more revisions than migrations`,
@@ -428,28 +499,28 @@ func TestExecutor(t *testing.T) {
 	require.Equal(t, drv.unlockCounter, 1)
 	require.True(t, drv.released())
 
-	// Incomplete revisions (error or ongoing) present.
-	rev1.ExecutionState = "error"
-	*rrw = mockRevisionReadWriter(migrate.Revisions{rev1})
+	// Failing, counter will be correct.
+	*rrw = mockRevisionReadWriter(migrate.Revisions{rev1, rev2})
 	*drv = lockMockDriver{&mockDriver{}}
-	require.EqualError(t, ex.ExecuteN(context.Background(), 0), fmt.Sprintf(
-		"sql/migrate: execute: dirty migration state: version %q has state %q",
-		rev1.Version, "error",
-	))
-
-	require.Equal(t, drv.lockCounter, 1)
-	require.Equal(t, drv.unlockCounter, 1)
-	require.True(t, drv.released())
-
-	rev1.ExecutionState = "ongoing"
-	require.EqualError(t, ex.ExecuteN(context.Background(), 0), fmt.Sprintf(
-		"sql/migrate: execute: dirty migration state: version %q has state %q",
-		rev1.Version, "ongoing",
-	))
+	drv.failOn(2, errors.New("this is an error"))
+	require.ErrorContains(t, ex.ExecuteN(context.Background(), 1), "this is an error")
+	revs, err := rrw.ReadRevisions(context.Background())
+	require.NoError(t, err)
+	requireEqualRevision(t, &migrate.Revision{
+		Version:     "3",
+		Description: "partly",
+		Applied:     1,
+		Total:       2,
+		Error:       "Statement:\nALTER TABLE t_sub ADD c4 int;\n\nError:\nthis is an error",
+	}, revs[len(revs)-1])
+
+	// Re-attempting to migrate will pick up where the execution was left off.
+	*drv = lockMockDriver{&mockDriver{}}
+	require.NoError(t, ex.ExecuteN(context.Background(), 1))
+	require.Equal(t, []string{"ALTER TABLE t_sub ADD c4 int;"}, drv.executed)
 
-	require.Equal(t, drv.lockCounter, 2)
-	require.Equal(t, drv.unlockCounter, 2)
-	require.True(t, drv.released())
+	// Everything is applied.
+	require.ErrorIs(t, ex.ExecuteN(context.Background(), 0), migrate.ErrNoPendingFiles)
 }
 
 type (
@@ -463,12 +534,25 @@ type (
 		locks         map[string]struct{}
 		lockCounter   int
 		unlockCounter int
+		failCounter   int
+		failWith      error
 	}
-	lockMockDriver  struct{ *mockDriver }
-	emptyMockDriver struct{ *lockMockDriver }
+	lockMockDriver struct{ *mockDriver }
 )
 
+// the nth call to ExecContext will fail with the given error.
+func (m *mockDriver) failOn(n int, err error) {
+	m.failCounter = n
+	m.failWith = err
+}
+
 func (m *mockDriver) ExecContext(_ context.Context, query string, _ ...interface{}) (sql.Result, error) {
+	if m.failCounter > 0 {
+		m.failCounter--
+		if m.failCounter == 0 {
+			return nil, m.failWith
+		}
+	}
 	m.executed = append(m.executed, query)
 	return nil, nil
 }
@@ -528,7 +612,16 @@ func (rrw *mockRevisionReadWriter) WriteRevision(_ context.Context, r *migrate.R
 	return nil
 }
 
-func (rrw *mockRevisionReadWriter) ReadRevisions(_ context.Context) (migrate.Revisions, error) {
+func (rrw *mockRevisionReadWriter) ReadRevision(_ context.Context, v string) (*migrate.Revision, error) {
+	for _, r := range migrate.Revisions(*rrw) {
+		if r.Version == v {
+			return r, nil
+		}
+	}
+	return nil, migrate.ErrNotExist
+}
+
+func (rrw *mockRevisionReadWriter) ReadRevisions(context.Context) (migrate.Revisions, error) {
 	return migrate.Revisions(*rrw), nil
 }
 
@@ -550,9 +643,12 @@ func requireEqualRevisions(t *testing.T, expected, actual migrate.Revisions) {
 func requireEqualRevision(t *testing.T, expected, actual *migrate.Revision) {
 	require.Equal(t, expected.Version, actual.Version)
 	require.Equal(t, expected.Description, actual.Description)
-	require.Equal(t, expected.ExecutionState, actual.ExecutionState)
+	require.Equal(t, expected.Applied, actual.Applied)
+	require.Equal(t, expected.Total, actual.Total)
 	require.Equal(t, expected.Error, actual.Error)
-	require.Equal(t, expected.Hash, actual.Hash)
+	if expected.Hash != "" {
+		require.Equal(t, expected.Hash, actual.Hash)
+	}
 	require.Equal(t, expected.OperatorVersion, actual.OperatorVersion)
 	require.Equal(t, expected.Meta, actual.Meta)
 }
diff --git a/sql/migrate/testdata/migrate/atlas.sum b/sql/migrate/testdata/migrate/atlas.sum
index 58bb0b1b..23c4fc95 100644
--- a/sql/migrate/testdata/migrate/atlas.sum
+++ b/sql/migrate/testdata/migrate/atlas.sum
@@ -1,5 +1,6 @@
-h1:0cmvdmEYFYPQVtN6KDezcRIIMLwpSz11Lm7FeHv1xGg=
+h1:rXkukFq7O3f2xgfZZi97Mh5Yd6MYRy6XmMYMV+Z9S9c=
 1_initial.down.sql h1:0zypK43rgPbgvVUgVJABGN25VgM1QSeU+LJDBb8cEQI=
 1_initial.up.sql h1:hFhs5XhRml4KTWGF5td6h1s7xNqAFnaEBbC5Y/NF7i4=
 sub/1.a_sub.up.sql h1:GomhYquMSavc1O/WeukxL02orVIHO2EUK6pWsedwELc=
 sub/2.10.x-20_description.sql h1:9HvZYZZMi9S7eLQvNAR9W9BxaMRvFz0A9TUmpdEHt4o=
+sub/3_partly.sql h1:WC7hyKTXrRB3woOqTAdc2m7XSLnbctgtF+W2ozDLUVc=
diff --git a/sql/migrate/testdata/migrate/sub/3_partly.sql b/sql/migrate/testdata/migrate/sub/3_partly.sql
new file mode 100644
index 00000000..5e3bf76a
--- /dev/null
+++ b/sql/migrate/testdata/migrate/sub/3_partly.sql
@@ -0,0 +1,4 @@
+-- add c3 column
+ALTER TABLE t_sub ADD c3 int;
+-- add c4 column
+ALTER TABLE t_sub ADD c4 int;
\ No newline at end of file
diff --git a/sql/migrate/testdata/migrate/sub/atlas.sum b/sql/migrate/testdata/migrate/sub/atlas.sum
index 365475e8..e8d63778 100644
--- a/sql/migrate/testdata/migrate/sub/atlas.sum
+++ b/sql/migrate/testdata/migrate/sub/atlas.sum
@@ -1,3 +1,4 @@
-h1:tD7n7qjPygOt0PwZollvsVjQ190HDU4TQRA86IpHfuw=
+h1:VpH77zWOBMwX5QhvnQo0XQvXCrOYZg4h1o0XlJTQnl0=
 1.a_sub.up.sql h1:nXyZR020M/mH7LxkoTkJr7BcQkipVg90imQ9I4595dw=
 2.10.x-20_description.sql h1:wQB3Vh3PHVXQg9OD3Gn7TBxbZN3r1Qb7TtAE1g3q9mQ=
+3_partly.sql h1:lHlMz6mEvBfvjry5lFXjs2vi6Et9xb9CWicaOXD42Qc=