Repository: ariga/atlas
Pull request #946 (2022-07-14 13:51:05)

doc/website/blog: add all posts to side nav
Before:
![Screen Shot 2022-07-14 at 17 03 24](https://user-images.githubusercontent.com/73284641/179001060-207bfb13-09f5-4395-b11b-5c1c892011c0.png)

After:
<img width="356" alt="Screen Shot 2022-07-14 at 17 36 27" src="https://user-images.githubusercontent.com/73284641/179008309-84f775cc-5b43-41a4-8529-5b97b067e59f.png">






Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5bdb035d..69d94b89 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -95,6 +95,23 @@ jobs:
         run: go test -race ./...
         working-directory: cmd/atlas
 
+  integration:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
+      - name: Run integration tests for HCL
+        working-directory: internal/integration/hclsqlspec
+        run: go test -race -count=2 -v ./...
+
   
   integration-mysql56:
     runs-on: ubuntu-latest
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/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index fea93549..2afc6e56 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -134,6 +134,8 @@ module.exports = {
         },
         blog: {
           showReadingTime: true,
+          blogSidebarTitle: 'All our posts',
+          blogSidebarCount: 'ALL',
         },
         theme: {
           customCss: require.resolve('./src/css/custom.css'),
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/ci/ci.tmpl b/internal/ci/ci.tmpl
index 80a0c049..c55dd4ee 100644
--- a/internal/ci/ci.tmpl
+++ b/internal/ci/ci.tmpl
@@ -95,6 +95,23 @@ jobs:
         run: go test -race ./...
         working-directory: cmd/atlas
 
+  integration:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2.3.4
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - uses: actions/cache@v2.1.6
+        with:
+          path: ~/go/pkg/mod
+          key: {{ "${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}" }}
+          restore-keys: |
+            {{ "${{ runner.os }}-go-" }}
+      - name: Run integration tests for HCL
+        working-directory: internal/integration/hclsqlspec
+        run: go test -race -count=2 -v ./...
+
   {{ range $ }}
   integration-{{ .Version }}:
     runs-on: ubuntu-latest
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/hclsqlspec/hclsqlspec_test.go b/internal/integration/hclsqlspec/hclsqlspec_test.go
index bfe269e7..3dac0075 100644
--- a/internal/integration/hclsqlspec/hclsqlspec_test.go
+++ b/internal/integration/hclsqlspec/hclsqlspec_test.go
@@ -20,22 +20,22 @@ import (
 var dialects = []struct {
 	name string
 	schemahcl.Marshaler
-	schemahcl.Evaluator
+	Eval func(b []byte, v interface{}, inp map[string]string) error
 }{
 	{
 		name:      "mysql",
 		Marshaler: mysql.MarshalHCL,
-		Evaluator: mysql.EvalHCL,
+		Eval:      mysql.EvalHCLBytes,
 	},
 	{
 		name:      "postgres",
 		Marshaler: postgres.MarshalHCL,
-		Evaluator: postgres.EvalHCL,
+		Eval:      postgres.EvalHCLBytes,
 	},
 	{
 		name:      "sqlite",
 		Marshaler: sqlite.MarshalHCL,
-		Evaluator: sqlite.EvalHCL,
+		Eval:      sqlite.EvalHCLBytes,
 	},
 }
 
@@ -371,9 +371,7 @@ table "t2" {
 	for _, tt := range dialects {
 		t.Run(tt.name, func(t *testing.T) {
 			var r schema.Realm
-			parsed, err := schemahcl.ParseBytes([]byte(f))
-			require.NoError(t, err)
-			err = tt.Eval(parsed, &r, nil)
+			err := tt.Eval([]byte(f), &r, nil)
 			require.NoError(t, err)
 			exp := &schema.Realm{
 				Schemas: []*schema.Schema{
@@ -399,9 +397,7 @@ table "t2" {
 			hcl, err := tt.MarshalSpec(&r)
 			require.NoError(t, err)
 			var after schema.Realm
-			parsed, err = schemahcl.ParseBytes(hcl)
-			require.NoError(t, err)
-			err = tt.Eval(parsed, &after, nil)
+			err = tt.Eval(hcl, &after, nil)
 			require.NoError(t, err)
 			require.EqualValues(t, exp, &after)
 		})
@@ -422,9 +418,7 @@ func TestUnsignedImmutability(t *testing.T) {
 schema "test" {
 }`
 	var s schema.Schema
-	parsed, err := schemahcl.ParseBytes([]byte(f))
-	require.NoError(t, err)
-	err = mysql.EvalHCL(parsed, &s, nil)
+	err := mysql.EvalHCLBytes([]byte(f), &s, nil)
 	require.NoError(t, err)
 	tbl := s.Tables[0]
 	require.EqualValues(t, &schema.IntegerType{T: "bigint", Unsigned: true}, tbl.Columns[0].Type.Type)
@@ -465,9 +459,7 @@ table "b" "users" {
 }
 `
 	var r schema.Realm
-	parsed, err := schemahcl.ParseBytes([]byte(h))
-	require.NoError(t, err)
-	err = mysql.EvalHCL(parsed, &r, nil)
+	err := mysql.EvalHCLBytes([]byte(h), &r, nil)
 	require.NoError(t, err)
 
 	require.EqualValues(t, r.Schemas[0].Tables[0].Columns[0], r.Schemas[1].Tables[0].ForeignKeys[0].RefColumns[0])
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=