Repository: ariga/atlas
Pull request #947 (2022-07-14 14:52:13)

sql/migrate: utilize partial hashes to check for changes in applied s…
…tatements of partially applied files

If a file has been partially applied, we now check if the already applied part of a migration file was edited and refuse to work, if the history was changed.

![image](https://user-images.githubusercontent.com/12862103/179011820-d50b514a-d682-4007-a0a9-41e55570cb64.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 cefdc808..51556823 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -3,7 +3,7 @@ module ariga.io/atlas/cmd/atlas
 go 1.18
 
 require (
-	ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7
+	ariga.io/atlas v0.5.1-0.20220714144633-63ec7fc8c49a
 	entgo.io/ent v0.11.1
 	github.com/fatih/color v1.13.0
 	github.com/go-sql-driver/mysql v1.6.0
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index c283642e..b2bbc61b 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -1,5 +1,7 @@
 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=
+ariga.io/atlas v0.5.1-0.20220714144633-63ec7fc8c49a h1:pi8bppDVEpHRRqUiRoBvbzFV0wirfNrSQxQp9Q6AHTU=
+ariga.io/atlas v0.5.1-0.20220714144633-63ec7fc8c49a/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=
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index ebb697bc..7cb0d36d 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -905,8 +905,8 @@ func (l *LogTTY) Log(e migrate.LogEntry) {
 		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, "%s%v %v migrations ok (%s)\n", indent2, dash, zero(l.fileCounter-1), red("1 with errors"))
+		fmt.Fprintf(l.out, "%s%v %v sql statements ok (%s)\n", indent2, dash, zero(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)
@@ -917,6 +917,13 @@ func (l *LogTTY) reportFileEnd() {
 	fmt.Fprintf(l.out, "%s%v ok (%v)\n", indent2, dash, yellow("%s", time.Since(l.fileStart)))
 }
 
+func zero(v int) int {
+	if v < 0 {
+		return 0
+	}
+	return v
+}
+
 func logFormat(out io.Writer) (migrate.Logger, error) {
 	switch MigrateFlags.LogFormat {
 	case logFormatTTY:
diff --git a/cmd/atlas/internal/migrate/ent/migrate/schema.go b/cmd/atlas/internal/migrate/ent/migrate/schema.go
index a96a5f3f..b7f39525 100644
--- a/cmd/atlas/internal/migrate/ent/migrate/schema.go
+++ b/cmd/atlas/internal/migrate/ent/migrate/schema.go
@@ -23,6 +23,7 @@ var (
 		{Name: "execution_time", Type: field.TypeInt64},
 		{Name: "error", Type: field.TypeString, Nullable: true},
 		{Name: "hash", Type: field.TypeString},
+		{Name: "partial_hashes", Type: field.TypeJSON, Nullable: true},
 		{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 275ae8e9..043bbb90 100644
--- a/cmd/atlas/internal/migrate/ent/mutation.go
+++ b/cmd/atlas/internal/migrate/ent/mutation.go
@@ -47,6 +47,7 @@ type RevisionMutation struct {
 	addexecution_time *time.Duration
 	error             *string
 	hash              *string
+	partial_hashes    *[]string
 	operator_version  *string
 	meta              *map[string]string
 	clearedFields     map[string]struct{}
@@ -484,6 +485,55 @@ func (m *RevisionMutation) ResetHash() {
 	m.hash = nil
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (m *RevisionMutation) SetPartialHashes(s []string) {
+	m.partial_hashes = &s
+}
+
+// PartialHashes returns the value of the "partial_hashes" field in the mutation.
+func (m *RevisionMutation) PartialHashes() (r []string, exists bool) {
+	v := m.partial_hashes
+	if v == nil {
+		return
+	}
+	return *v, true
+}
+
+// OldPartialHashes returns the old "partial_hashes" 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) OldPartialHashes(ctx context.Context) (v []string, err error) {
+	if !m.op.Is(OpUpdateOne) {
+		return v, errors.New("OldPartialHashes is only allowed on UpdateOne operations")
+	}
+	if m.id == nil || m.oldValue == nil {
+		return v, errors.New("OldPartialHashes requires an ID field in the mutation")
+	}
+	oldValue, err := m.oldValue(ctx)
+	if err != nil {
+		return v, fmt.Errorf("querying old value for OldPartialHashes: %w", err)
+	}
+	return oldValue.PartialHashes, nil
+}
+
+// ClearPartialHashes clears the value of the "partial_hashes" field.
+func (m *RevisionMutation) ClearPartialHashes() {
+	m.partial_hashes = nil
+	m.clearedFields[revision.FieldPartialHashes] = struct{}{}
+}
+
+// PartialHashesCleared returns if the "partial_hashes" field was cleared in this mutation.
+func (m *RevisionMutation) PartialHashesCleared() bool {
+	_, ok := m.clearedFields[revision.FieldPartialHashes]
+	return ok
+}
+
+// ResetPartialHashes resets all changes to the "partial_hashes" field.
+func (m *RevisionMutation) ResetPartialHashes() {
+	m.partial_hashes = nil
+	delete(m.clearedFields, revision.FieldPartialHashes)
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (m *RevisionMutation) SetOperatorVersion(s string) {
 	m.operator_version = &s
@@ -575,7 +625,7 @@ func (m *RevisionMutation) Type() string {
 // order to get all numeric fields that were incremented/decremented, call
 // AddedFields().
 func (m *RevisionMutation) Fields() []string {
-	fields := make([]string, 0, 9)
+	fields := make([]string, 0, 10)
 	if m.description != nil {
 		fields = append(fields, revision.FieldDescription)
 	}
@@ -597,6 +647,9 @@ func (m *RevisionMutation) Fields() []string {
 	if m.hash != nil {
 		fields = append(fields, revision.FieldHash)
 	}
+	if m.partial_hashes != nil {
+		fields = append(fields, revision.FieldPartialHashes)
+	}
 	if m.operator_version != nil {
 		fields = append(fields, revision.FieldOperatorVersion)
 	}
@@ -625,6 +678,8 @@ func (m *RevisionMutation) Field(name string) (ent.Value, bool) {
 		return m.Error()
 	case revision.FieldHash:
 		return m.Hash()
+	case revision.FieldPartialHashes:
+		return m.PartialHashes()
 	case revision.FieldOperatorVersion:
 		return m.OperatorVersion()
 	case revision.FieldMeta:
@@ -652,6 +707,8 @@ func (m *RevisionMutation) OldField(ctx context.Context, name string) (ent.Value
 		return m.OldError(ctx)
 	case revision.FieldHash:
 		return m.OldHash(ctx)
+	case revision.FieldPartialHashes:
+		return m.OldPartialHashes(ctx)
 	case revision.FieldOperatorVersion:
 		return m.OldOperatorVersion(ctx)
 	case revision.FieldMeta:
@@ -714,6 +771,13 @@ func (m *RevisionMutation) SetField(name string, value ent.Value) error {
 		}
 		m.SetHash(v)
 		return nil
+	case revision.FieldPartialHashes:
+		v, ok := value.([]string)
+		if !ok {
+			return fmt.Errorf("unexpected type %T for field %s", value, name)
+		}
+		m.SetPartialHashes(v)
+		return nil
 	case revision.FieldOperatorVersion:
 		v, ok := value.(string)
 		if !ok {
@@ -800,6 +864,9 @@ func (m *RevisionMutation) ClearedFields() []string {
 	if m.FieldCleared(revision.FieldError) {
 		fields = append(fields, revision.FieldError)
 	}
+	if m.FieldCleared(revision.FieldPartialHashes) {
+		fields = append(fields, revision.FieldPartialHashes)
+	}
 	return fields
 }
 
@@ -817,6 +884,9 @@ func (m *RevisionMutation) ClearField(name string) error {
 	case revision.FieldError:
 		m.ClearError()
 		return nil
+	case revision.FieldPartialHashes:
+		m.ClearPartialHashes()
+		return nil
 	}
 	return fmt.Errorf("unknown Revision nullable field %s", name)
 }
@@ -846,6 +916,9 @@ func (m *RevisionMutation) ResetField(name string) error {
 	case revision.FieldHash:
 		m.ResetHash()
 		return nil
+	case revision.FieldPartialHashes:
+		m.ResetPartialHashes()
+		return nil
 	case revision.FieldOperatorVersion:
 		m.ResetOperatorVersion()
 		return nil
diff --git a/cmd/atlas/internal/migrate/ent/revision.go b/cmd/atlas/internal/migrate/ent/revision.go
index 3b5bb679..ad1d4064 100644
--- a/cmd/atlas/internal/migrate/ent/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision.go
@@ -35,6 +35,8 @@ type Revision struct {
 	Error string `json:"error,omitempty"`
 	// Hash holds the value of the "hash" field.
 	Hash string `json:"hash,omitempty"`
+	// PartialHashes holds the value of the "partial_hashes" field.
+	PartialHashes []string `json:"partial_hashes,omitempty"`
 	// OperatorVersion holds the value of the "operator_version" field.
 	OperatorVersion string `json:"operator_version,omitempty"`
 	// Meta holds the value of the "meta" field.
@@ -46,7 +48,7 @@ func (*Revision) scanValues(columns []string) ([]interface{}, error) {
 	values := make([]interface{}, len(columns))
 	for i := range columns {
 		switch columns[i] {
-		case revision.FieldMeta:
+		case revision.FieldPartialHashes, revision.FieldMeta:
 			values[i] = new([]byte)
 		case revision.FieldApplied, revision.FieldTotal, revision.FieldExecutionTime:
 			values[i] = new(sql.NullInt64)
@@ -117,6 +119,14 @@ func (r *Revision) assignValues(columns []string, values []interface{}) error {
 			} else if value.Valid {
 				r.Hash = value.String
 			}
+		case revision.FieldPartialHashes:
+			if value, ok := values[i].(*[]byte); !ok {
+				return fmt.Errorf("unexpected type %T for field partial_hashes", values[i])
+			} else if value != nil && len(*value) > 0 {
+				if err := json.Unmarshal(*value, &r.PartialHashes); err != nil {
+					return fmt.Errorf("unmarshal field partial_hashes: %w", err)
+				}
+			}
 		case revision.FieldOperatorVersion:
 			if value, ok := values[i].(*sql.NullString); !ok {
 				return fmt.Errorf("unexpected type %T for field operator_version", values[i])
@@ -180,6 +190,9 @@ func (r *Revision) String() string {
 	builder.WriteString("hash=")
 	builder.WriteString(r.Hash)
 	builder.WriteString(", ")
+	builder.WriteString("partial_hashes=")
+	builder.WriteString(fmt.Sprintf("%v", r.PartialHashes))
+	builder.WriteString(", ")
 	builder.WriteString("operator_version=")
 	builder.WriteString(r.OperatorVersion)
 	builder.WriteString(", ")
diff --git a/cmd/atlas/internal/migrate/ent/revision/revision.go b/cmd/atlas/internal/migrate/ent/revision/revision.go
index 7b186758..9afefe1f 100644
--- a/cmd/atlas/internal/migrate/ent/revision/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision/revision.go
@@ -25,6 +25,8 @@ const (
 	FieldError = "error"
 	// FieldHash holds the string denoting the hash field in the database.
 	FieldHash = "hash"
+	// FieldPartialHashes holds the string denoting the partial_hashes field in the database.
+	FieldPartialHashes = "partial_hashes"
 	// FieldOperatorVersion holds the string denoting the operator_version field in the database.
 	FieldOperatorVersion = "operator_version"
 	// FieldMeta holds the string denoting the meta field in the database.
@@ -43,6 +45,7 @@ var Columns = []string{
 	FieldExecutionTime,
 	FieldError,
 	FieldHash,
+	FieldPartialHashes,
 	FieldOperatorVersion,
 	FieldMeta,
 }
diff --git a/cmd/atlas/internal/migrate/ent/revision/where.go b/cmd/atlas/internal/migrate/ent/revision/where.go
index 53846132..a3858dcc 100644
--- a/cmd/atlas/internal/migrate/ent/revision/where.go
+++ b/cmd/atlas/internal/migrate/ent/revision/where.go
@@ -798,6 +798,20 @@ func HashContainsFold(v string) predicate.Revision {
 	})
 }
 
+// PartialHashesIsNil applies the IsNil predicate on the "partial_hashes" field.
+func PartialHashesIsNil() predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.IsNull(s.C(FieldPartialHashes)))
+	})
+}
+
+// PartialHashesNotNil applies the NotNil predicate on the "partial_hashes" field.
+func PartialHashesNotNil() predicate.Revision {
+	return predicate.Revision(func(s *sql.Selector) {
+		s.Where(sql.NotNull(s.C(FieldPartialHashes)))
+	})
+}
+
 // OperatorVersionEQ applies the EQ predicate on the "operator_version" field.
 func OperatorVersionEQ(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 84cc17ad..7ede34b0 100644
--- a/cmd/atlas/internal/migrate/ent/revision_create.go
+++ b/cmd/atlas/internal/migrate/ent/revision_create.go
@@ -77,6 +77,12 @@ func (rc *RevisionCreate) SetHash(s string) *RevisionCreate {
 	return rc
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (rc *RevisionCreate) SetPartialHashes(s []string) *RevisionCreate {
+	rc.mutation.SetPartialHashes(s)
+	return rc
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (rc *RevisionCreate) SetOperatorVersion(s string) *RevisionCreate {
 	rc.mutation.SetOperatorVersion(s)
@@ -294,6 +300,14 @@ func (rc *RevisionCreate) createSpec() (*Revision, *sqlgraph.CreateSpec) {
 		})
 		_node.Hash = value
 	}
+	if value, ok := rc.mutation.PartialHashes(); ok {
+		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
+			Type:   field.TypeJSON,
+			Value:  value,
+			Column: revision.FieldPartialHashes,
+		})
+		_node.PartialHashes = value
+	}
 	if value, ok := rc.mutation.OperatorVersion(); ok {
 		_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -472,6 +486,24 @@ func (u *RevisionUpsert) UpdateHash() *RevisionUpsert {
 	return u
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (u *RevisionUpsert) SetPartialHashes(v []string) *RevisionUpsert {
+	u.Set(revision.FieldPartialHashes, v)
+	return u
+}
+
+// UpdatePartialHashes sets the "partial_hashes" field to the value that was provided on create.
+func (u *RevisionUpsert) UpdatePartialHashes() *RevisionUpsert {
+	u.SetExcluded(revision.FieldPartialHashes)
+	return u
+}
+
+// ClearPartialHashes clears the value of the "partial_hashes" field.
+func (u *RevisionUpsert) ClearPartialHashes() *RevisionUpsert {
+	u.SetNull(revision.FieldPartialHashes)
+	return u
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (u *RevisionUpsert) SetOperatorVersion(v string) *RevisionUpsert {
 	u.Set(revision.FieldOperatorVersion, v)
@@ -678,6 +710,27 @@ func (u *RevisionUpsertOne) UpdateHash() *RevisionUpsertOne {
 	})
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (u *RevisionUpsertOne) SetPartialHashes(v []string) *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetPartialHashes(v)
+	})
+}
+
+// UpdatePartialHashes sets the "partial_hashes" field to the value that was provided on create.
+func (u *RevisionUpsertOne) UpdatePartialHashes() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdatePartialHashes()
+	})
+}
+
+// ClearPartialHashes clears the value of the "partial_hashes" field.
+func (u *RevisionUpsertOne) ClearPartialHashes() *RevisionUpsertOne {
+	return u.Update(func(s *RevisionUpsert) {
+		s.ClearPartialHashes()
+	})
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (u *RevisionUpsertOne) SetOperatorVersion(v string) *RevisionUpsertOne {
 	return u.Update(func(s *RevisionUpsert) {
@@ -1053,6 +1106,27 @@ func (u *RevisionUpsertBulk) UpdateHash() *RevisionUpsertBulk {
 	})
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (u *RevisionUpsertBulk) SetPartialHashes(v []string) *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.SetPartialHashes(v)
+	})
+}
+
+// UpdatePartialHashes sets the "partial_hashes" field to the value that was provided on create.
+func (u *RevisionUpsertBulk) UpdatePartialHashes() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.UpdatePartialHashes()
+	})
+}
+
+// ClearPartialHashes clears the value of the "partial_hashes" field.
+func (u *RevisionUpsertBulk) ClearPartialHashes() *RevisionUpsertBulk {
+	return u.Update(func(s *RevisionUpsert) {
+		s.ClearPartialHashes()
+	})
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (u *RevisionUpsertBulk) SetOperatorVersion(v string) *RevisionUpsertBulk {
 	return u.Update(func(s *RevisionUpsert) {
diff --git a/cmd/atlas/internal/migrate/ent/revision_update.go b/cmd/atlas/internal/migrate/ent/revision_update.go
index 1af508cd..a8738986 100644
--- a/cmd/atlas/internal/migrate/ent/revision_update.go
+++ b/cmd/atlas/internal/migrate/ent/revision_update.go
@@ -98,6 +98,18 @@ func (ru *RevisionUpdate) SetHash(s string) *RevisionUpdate {
 	return ru
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (ru *RevisionUpdate) SetPartialHashes(s []string) *RevisionUpdate {
+	ru.mutation.SetPartialHashes(s)
+	return ru
+}
+
+// ClearPartialHashes clears the value of the "partial_hashes" field.
+func (ru *RevisionUpdate) ClearPartialHashes() *RevisionUpdate {
+	ru.mutation.ClearPartialHashes()
+	return ru
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (ru *RevisionUpdate) SetOperatorVersion(s string) *RevisionUpdate {
 	ru.mutation.SetOperatorVersion(s)
@@ -265,6 +277,19 @@ func (ru *RevisionUpdate) sqlSave(ctx context.Context) (n int, err error) {
 			Column: revision.FieldHash,
 		})
 	}
+	if value, ok := ru.mutation.PartialHashes(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeJSON,
+			Value:  value,
+			Column: revision.FieldPartialHashes,
+		})
+	}
+	if ru.mutation.PartialHashesCleared() {
+		_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
+			Type:   field.TypeJSON,
+			Column: revision.FieldPartialHashes,
+		})
+	}
 	if value, ok := ru.mutation.OperatorVersion(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
@@ -365,6 +390,18 @@ func (ruo *RevisionUpdateOne) SetHash(s string) *RevisionUpdateOne {
 	return ruo
 }
 
+// SetPartialHashes sets the "partial_hashes" field.
+func (ruo *RevisionUpdateOne) SetPartialHashes(s []string) *RevisionUpdateOne {
+	ruo.mutation.SetPartialHashes(s)
+	return ruo
+}
+
+// ClearPartialHashes clears the value of the "partial_hashes" field.
+func (ruo *RevisionUpdateOne) ClearPartialHashes() *RevisionUpdateOne {
+	ruo.mutation.ClearPartialHashes()
+	return ruo
+}
+
 // SetOperatorVersion sets the "operator_version" field.
 func (ruo *RevisionUpdateOne) SetOperatorVersion(s string) *RevisionUpdateOne {
 	ruo.mutation.SetOperatorVersion(s)
@@ -562,6 +599,19 @@ func (ruo *RevisionUpdateOne) sqlSave(ctx context.Context) (_node *Revision, err
 			Column: revision.FieldHash,
 		})
 	}
+	if value, ok := ruo.mutation.PartialHashes(); ok {
+		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
+			Type:   field.TypeJSON,
+			Value:  value,
+			Column: revision.FieldPartialHashes,
+		})
+	}
+	if ruo.mutation.PartialHashesCleared() {
+		_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
+			Type:   field.TypeJSON,
+			Column: revision.FieldPartialHashes,
+		})
+	}
 	if value, ok := ruo.mutation.OperatorVersion(); ok {
 		_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
 			Type:   field.TypeString,
diff --git a/cmd/atlas/internal/migrate/ent/schema/revision.go b/cmd/atlas/internal/migrate/ent/schema/revision.go
index a5b5bb0b..4d237c6e 100644
--- a/cmd/atlas/internal/migrate/ent/schema/revision.go
+++ b/cmd/atlas/internal/migrate/ent/schema/revision.go
@@ -36,6 +36,8 @@ func (Revision) Fields() []ent.Field {
 		field.String("error").
 			Optional(),
 		field.String("hash"),
+		field.Strings("partial_hashes").
+			Optional(),
 		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 1f310e85..093c20f4 100644
--- a/cmd/atlas/internal/migrate/migrate.go
+++ b/cmd/atlas/internal/migrate/migrate.go
@@ -157,6 +157,7 @@ func (r *EntRevisions) write(ctx context.Context, rev *migrate.Revision) error {
 		SetExecutionTime(rev.ExecutionTime).
 		SetError(rev.Error).
 		SetHash(rev.Hash).
+		SetPartialHashes(rev.PartialHashes).
 		SetOperatorVersion(rev.OperatorVersion).
 		SetMeta(rev.Meta).
 		OnConflict(sql.ConflictColumns(revision.FieldID)).
@@ -174,6 +175,7 @@ func fromEnt(r *ent.Revision) *migrate.Revision {
 		ExecutionTime:   r.ExecutionTime,
 		Error:           r.Error,
 		Hash:            r.Hash,
+		PartialHashes:   r.PartialHashes,
 		OperatorVersion: r.OperatorVersion,
 		Meta:            r.Meta,
 	}
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/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 38a5e3f2..8e0539cc 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -7,7 +7,7 @@ replace ariga.io/atlas => ../../
 replace ariga.io/atlas/cmd/atlas => ../../cmd/atlas
 
 require (
-	ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7
+	ariga.io/atlas v0.5.1-0.20220714144633-63ec7fc8c49a
 	entgo.io/ent v0.11.1
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/hashicorp/hcl/v2 v2.13.0
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index f7d3caae..bf7e7abd 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -221,6 +221,8 @@ type (
 		Error string
 		// Hash is the check-sum of this migration as stated by the migration directories HashFile.
 		Hash string
+		// PartialHashes contains one hash per statement that has been applied on the database.
+		PartialHashes []string
 		// OperatorVersion holds a string representation of the Atlas operator managing this database migration.
 		OperatorVersion string
 		// Meta holds additional custom meta-data given for this migration.
@@ -450,8 +452,8 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 	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.
+// Execute executes the given migration file on the database. If it sees a file, that has been partially applied, it
+// will continue with the next statement in line.
 func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 	hf, err := HashSum(e.dir)
 	if err != nil {
@@ -473,6 +475,17 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning statements from %q: %w", m.Name(), err)
 	}
+	// Create checksums for the statements.
+	var (
+		sums = make([]string, len(stmts))
+		h    = sha256.New()
+	)
+	for i, stmt := range stmts {
+		if _, err := h.Write([]byte(stmt)); err != nil {
+			return err
+		}
+		sums[i] = base64.StdEncoding.EncodeToString(h.Sum(nil))
+	}
 	// 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)
@@ -499,18 +512,26 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 			err = wrap(err2, err)
 		}
 	}(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
+	if r.Applied > 0 {
+		// If the file has been applied partially before, check if the
+		// applied statements were not changed.
+		for i := 0; i < r.Applied; i++ {
+			if i > len(sums) || sums[i] != strings.TrimPrefix(r.PartialHashes[i], "h1:") {
+				err = HistoryChangedError{m.Name(), i + 1}
+				e.log.Log(LogError{Error: err})
+				return err
+			}
 		}
+	}
+	e.log.Log(LogFile{r.Version, r.Description, r.Applied})
+	for _, stmt := range stmts[r.Applied:] {
 		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.PartialHashes = append(r.PartialHashes, "h1:"+sums[r.Applied])
 		r.Applied++
 		if err = writeRevision(ctx, e.rrw, r); err != nil {
 			return err
@@ -527,6 +548,16 @@ func writeRevision(ctx context.Context, w RevisionReadWriter, r *Revision) error
 	return nil
 }
 
+// HistoryChangedError is returned if between two execution attempts already applied statements of a file have changed.
+type HistoryChangedError struct {
+	File string
+	Stmt int
+}
+
+func (e HistoryChangedError) Error() string {
+	return fmt.Sprintf("sql/migrate: execute: history changed: statement %d from file %q changed", e.Stmt, e.File)
+}
+
 // 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 {


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index b074a6f5..ff1e2998 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -168,11 +168,23 @@ func TestMigrate_Apply(t *testing.T) {
 	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.
+	// Editing an applied line will raise error.
 	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/col_2/col_5/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.ErrorAs(t, err, &migrate.HistoryChangedError{})
+
+	// Fixing the migration file will finish without errors.
+	require.NoError(t, exec.Command("sed", "-i", "s/col_5/col_2/g", "testdata/sqlite3/20220318104615_second.sql").Run())
 	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)
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/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index 1ff28920..2c48a7a7 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -514,7 +514,13 @@ func TestExecutor(t *testing.T) {
 		Error:       "Statement:\nALTER TABLE t_sub ADD c4 int;\n\nError:\nthis is an error",
 	}, revs[len(revs)-1])
 
+	// Will fail if applied contents hash has changed (like when editing a partially applied file to fix an error).
+	h := revs[len(revs)-1].PartialHashes[0]
+	revs[len(revs)-1].PartialHashes[0] += h
+	require.ErrorAs(t, ex.ExecuteN(context.Background(), 1), &migrate.HistoryChangedError{})
+
 	// Re-attempting to migrate will pick up where the execution was left off.
+	revs[len(revs)-1].PartialHashes[0] = h
 	*drv = lockMockDriver{&mockDriver{}}
 	require.NoError(t, ex.ExecuteN(context.Background(), 1))
 	require.Equal(t, []string{"ALTER TABLE t_sub ADD c4 int;"}, drv.executed)