Repository: ariga/atlas
Pull request #937 (2022-07-12 10:41:12)

sql/sqlcheck: move analyzers to be driver-specific
Also, detect modify tables changes and ignore report them as destructive. See examples for before/after:

![image](https://user-images.githubusercontent.com/7413593/178472455-9a8e4136-2b85-4fe9-a1bc-1dce033e5ab8.png)

![image](https://user-images.githubusercontent.com/7413593/178472527-64c4ebe0-cf06-4505-aa4f-a7195274c288.png)




Patch:
diff --git a/cmd/atlas/go.work.sum b/cmd/atlas/go.work.sum
new file mode 100644
index 00000000..56f9d847
--- /dev/null
+++ b/cmd/atlas/go.work.sum
@@ -0,0 +1,1 @@
+github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index ec212bda..9d4d13e3 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -23,8 +23,6 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
-	"ariga.io/atlas/sql/sqlcheck/datadepend"
-	"ariga.io/atlas/sql/sqlcheck/destructive"
 	"ariga.io/atlas/sql/sqlclient"
 	"ariga.io/atlas/sql/sqltool"
 	"github.com/fatih/color"
@@ -579,6 +577,10 @@ func CmdMigrateLintRun(cmd *cobra.Command, _ []string) error {
 			return fmt.Errorf("parse log format: %w", err)
 		}
 	}
+	az, err := sqlcheck.AnalyzerFor(dev.Name, nil)
+	if err != nil {
+		return err
+	}
 	r := &ci.Runner{
 		Dev:            dev,
 		Dir:            local,
@@ -587,10 +589,7 @@ func CmdMigrateLintRun(cmd *cobra.Command, _ []string) error {
 			T: format,
 			W: cmd.OutOrStdout(),
 		},
-		Analyzer: sqlcheck.Analyzers{
-			datadepend.New(datadepend.Options{}),
-			destructive.New(destructive.Options{}),
-		},
+		Analyzer: az,
 	}
 	return r.Run(cmd.Context())
 }
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index be868121..44b06157 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -181,7 +181,7 @@ func selectEnv(selected string) (*Env, error) {
 		return env, nil
 	}
 	if _, err := os.Stat(projectFileName); os.IsNotExist(err) {
-		return env, nil
+		return nil, fmt.Errorf("project file %q was not found", projectFileName)
 	}
 	return LoadEnv(projectFileName, selected, WithInput(GlobalFlags.Vars))
 }
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index f2258c00..a68c5578 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -12,8 +12,11 @@ import (
 	"ariga.io/atlas/cmd/atlas/internal/cmdapi"
 	_ "ariga.io/atlas/cmd/atlas/internal/docker"
 	_ "ariga.io/atlas/sql/mysql"
+	_ "ariga.io/atlas/sql/mysql/mysqlcheck"
 	_ "ariga.io/atlas/sql/postgres"
+	_ "ariga.io/atlas/sql/postgres/postgrescheck"
 	_ "ariga.io/atlas/sql/sqlite"
+	_ "ariga.io/atlas/sql/sqlite/sqlitecheck"
 
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 4be7a442..99bf0764 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -38,9 +38,12 @@ type (
 	}
 )
 
+// DriverName holds the name used for registration.
+const DriverName = "mysql"
+
 func init() {
 	sqlclient.Register(
-		"mysql",
+		DriverName,
 		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterFlavours("maria", "mariadb"),
diff --git a/sql/mysql/internal/mysqlcheck/datadepend.go b/sql/mysql/mysqlcheck/mysqlcheck.go
similarity index 86%
rename from sql/mysql/internal/mysqlcheck/datadepend.go
rename to sql/mysql/mysqlcheck/mysqlcheck.go
index 9897d5d5..79752b05 100644
--- a/sql/mysql/internal/mysqlcheck/datadepend.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck.go
@@ -15,14 +15,15 @@ import (
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
 	"ariga.io/atlas/sql/sqlcheck/datadepend"
+	"ariga.io/atlas/sql/sqlcheck/destructive"
 )
 
 type (
-	// DataDependOptions defines the configuration options
-	// for the data-dependent changes checker.
+	// DataDependOptions defines the additional configuration
+	// options for the data-dependent changes checker.
 	DataDependOptions struct {
-		// NotNull indicates if the analyzer should check for modification or
-		// addition of not null constraints to
+		// NotNull indicates if the analyzer should check for modification
+		// or addition of NOT NULL constraints to columns.
 		NotNull *bool `spec:"not_null,omitempty"`
 
 		// Allow drivers to extend the configuration.
@@ -36,6 +37,15 @@ type (
 	}
 )
 
+// NewDataDepend creates new data-depend analyzer.
+func NewDataDepend(*schemahcl.Resource) (*DataDepend, error) {
+	notnull := true
+	return &DataDepend{
+		Analyzer:          datadepend.New(datadepend.Options{}),
+		DataDependOptions: DataDependOptions{NotNull: &notnull},
+	}, nil
+}
+
 // Analyze runs data-depend analysis on MySQL changes.
 func (d *DataDepend) Analyze(ctx context.Context, p *sqlcheck.Pass) error {
 	diags := d.Diagnostics(ctx, p)
@@ -61,22 +71,6 @@ func (d *DataDepend) Analyze(ctx context.Context, p *sqlcheck.Pass) error {
 	return nil
 }
 
-// RegisterDataDepend is the constructor used in datadepend.Register.
-func RegisterDataDepend(ctx context.Context, az *datadepend.Analyzer, pass *sqlcheck.Pass) error {
-	var (
-		notnull = true
-		opts    DataDependOptions
-	)
-	if err := az.Extra.As(&opts); err != nil {
-		return err
-	}
-	if opts.NotNull == nil {
-		opts.NotNull = &notnull
-	}
-	d := &DataDepend{Analyzer: az, DataDependOptions: opts}
-	return d.Analyze(ctx, pass)
-}
-
 func (d *DataDepend) addColumn(p *sqlcheck.Pass, change *sqlcheck.Change, t *schema.Table, c *schema.Column) (diags []sqlcheck.Diagnostic, err error) {
 	// Skip this check in case it is disabled, the column is nullable
 	// or with default value, or the table was added in this file.
@@ -188,3 +182,16 @@ func (d *DataDepend) addColumn(p *sqlcheck.Pass, change *sqlcheck.Change, t *sch
 	}
 	return
 }
+
+func init() {
+	sqlcheck.Register(mysql.DriverName, func(r *schemahcl.Resource) (sqlcheck.Analyzer, error) {
+		az, err := NewDataDepend(r)
+		if err != nil {
+			return nil, err
+		}
+		return sqlcheck.Analyzers{
+			destructive.New(destructive.Options{}),
+			az,
+		}, nil
+	})
+}
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index dc5e2914..f8323037 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -40,9 +40,12 @@ type (
 	}
 )
 
+// DriverName holds the name used for registration.
+const DriverName = "postgres"
+
 func init() {
 	sqlclient.Register(
-		"postgres",
+		DriverName,
 		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterURLParser(parser{}),
diff --git a/sql/postgres/postgrescheck/postgrescheck.go b/sql/postgres/postgrescheck/postgrescheck.go
new file mode 100644
index 00000000..12f94441
--- /dev/null
+++ b/sql/postgres/postgrescheck/postgrescheck.go
@@ -0,0 +1,22 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package sqlitecheck
+
+import (
+	"ariga.io/atlas/schemahcl"
+	"ariga.io/atlas/sql/sqlcheck"
+	"ariga.io/atlas/sql/sqlcheck/datadepend"
+	"ariga.io/atlas/sql/sqlcheck/destructive"
+	"ariga.io/atlas/sql/sqlite"
+)
+
+func init() {
+	sqlcheck.Register(sqlite.DriverName, func(*schemahcl.Resource) (sqlcheck.Analyzer, error) {
+		return sqlcheck.Analyzers{
+			destructive.New(destructive.Options{}),
+			datadepend.New(datadepend.Options{}),
+		}, nil
+	})
+}
diff --git a/sql/sqlcheck/datadepend/datadepend.go b/sql/sqlcheck/datadepend/datadepend.go
index ecff0ae1..b53693fe 100644
--- a/sql/sqlcheck/datadepend/datadepend.go
+++ b/sql/sqlcheck/datadepend/datadepend.go
@@ -7,7 +7,6 @@ package datadepend
 import (
 	"context"
 	"fmt"
-	"sync"
 
 	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/schema"
@@ -43,11 +42,6 @@ func New(opts Options) *Analyzer {
 
 // Analyze implements sqlcheck.Analyzer.
 func (a *Analyzer) Analyze(ctx context.Context, p *sqlcheck.Pass) error {
-	f, ok := drivers.Load(p.Dev.Name)
-	if ok {
-		return f.(func(context.Context, *Analyzer, *sqlcheck.Pass) error)(ctx, a, p)
-	}
-	// Fallback to the default implementation.
 	a.Report(p, a.Diagnostics(ctx, p))
 	return nil
 }
@@ -109,12 +103,3 @@ func (a *Analyzer) Report(p *sqlcheck.Pass, diags []sqlcheck.Diagnostic) {
 		})
 	}
 }
-
-// drivers specific analyzers.
-var drivers sync.Map
-
-// Register allows drivers to override the
-// default analysis with custom behavior.
-func Register(name string, f func(context.Context, *Analyzer, *sqlcheck.Pass) error) {
-	drivers.Store(name, f)
-}
diff --git a/sql/sqlcheck/destructive/destructive.go b/sql/sqlcheck/destructive/destructive.go
index 0874c642..65df5bcc 100644
--- a/sql/sqlcheck/destructive/destructive.go
+++ b/sql/sqlcheck/destructive/destructive.go
@@ -47,14 +47,14 @@ func New(opts Options) *Analyzer {
 	return d
 }
 
-// Analyze implements Analyzer.
-func (d *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
+// Analyze implements sqlcheck.Analyzer.
+func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 	var diags []sqlcheck.Diagnostic
 	for _, sc := range p.File.Changes {
 		for _, c := range sc.Changes {
 			switch c := c.(type) {
 			case *schema.DropSchema:
-				if *d.DropSchema && p.File.SchemaSpan(c.S) != sqlcheck.SpanTemporary {
+				if *a.DropSchema && p.File.SchemaSpan(c.S) != sqlcheck.SpanTemporary {
 					var text string
 					switch n := len(c.S.Tables); {
 					case n == 0:
@@ -67,14 +67,14 @@ func (d *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 					diags = append(diags, sqlcheck.Diagnostic{Pos: sc.Pos, Text: text})
 				}
 			case *schema.DropTable:
-				if *d.DropTable && p.File.SchemaSpan(c.T.Schema) != sqlcheck.SpanDropped && p.File.TableSpan(c.T) != sqlcheck.SpanTemporary {
+				if *a.DropTable && p.File.SchemaSpan(c.T.Schema) != sqlcheck.SpanDropped && p.File.TableSpan(c.T) != sqlcheck.SpanTemporary {
 					diags = append(diags, sqlcheck.Diagnostic{
 						Pos:  sc.Pos,
 						Text: fmt.Sprintf("Dropping table %q", c.T.Name),
 					})
 				}
 			case *schema.ModifyTable:
-				if !*d.DropColumn {
+				if !*a.DropColumn {
 					continue
 				}
 				for i := range c.Changes {
diff --git a/sql/sqlcheck/sqlcheck.go b/sql/sqlcheck/sqlcheck.go
index 0c219f96..08a8fe71 100644
--- a/sql/sqlcheck/sqlcheck.go
+++ b/sql/sqlcheck/sqlcheck.go
@@ -12,7 +12,9 @@ package sqlcheck
 
 import (
 	"context"
+	"sync"
 
+	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
@@ -92,6 +94,14 @@ func (a Analyzers) Analyze(ctx context.Context, p *Pass) error {
 	return nil
 }
 
+// AnalyzerFunc allows using ordinary functions as analyzers.
+type AnalyzerFunc func(ctx context.Context, p *Pass) error
+
+// Analyze calls f.
+func (f AnalyzerFunc) Analyze(ctx context.Context, p *Pass) error {
+	return f(ctx, p)
+}
+
 // ReportWriterFunc is a function that implements Reporter.
 type ReportWriterFunc func(Report)
 
@@ -215,3 +225,23 @@ func (f *File) tableSpan(t *schema.Table) *tableSpan {
 	}
 	return f.spans[t.Schema.Name].tables[t.Name]
 }
+
+// drivers specific analyzers.
+var drivers sync.Map
+
+// Register allows drivers to register a constructor function for creating
+// analyzers from the given HCL resource.
+func Register(name string, f func(*schemahcl.Resource) (Analyzer, error)) {
+	drivers.Store(name, f)
+}
+
+// AnalyzerFor instantiates a new Analyzer from the given HCL resource
+// based on the registered constructor function.
+func AnalyzerFor(name string, r *schemahcl.Resource) (Analyzer, error) {
+	f, ok := drivers.Load(name)
+	if ok {
+		return f.(func(*schemahcl.Resource) (Analyzer, error))(r)
+	}
+	// A nop analyzer.
+	return (Analyzers)(nil), nil
+}
diff --git a/sql/sqlite/diff.go b/sql/sqlite/diff.go
index cda60702..cac49273 100644
--- a/sql/sqlite/diff.go
+++ b/sql/sqlite/diff.go
@@ -14,17 +14,17 @@ import (
 	"ariga.io/atlas/sql/schema"
 )
 
-// A diff provides a SQLite implementation for sqlx.DiffDriver.
-type diff struct{ conn }
+// A Diff provides a SQLite implementation for sqlx.DiffDriver.
+type Diff struct{}
 
 // SchemaAttrDiff returns a changeset for migrating schema attributes from one state to the other.
-func (d *diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
+func (d *Diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
 	// No special schema attribute diffing for SQLite.
 	return nil
 }
 
 // TableAttrDiff returns a changeset for migrating table attributes from one state to the other.
-func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
+func (d *Diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 	var changes []schema.Change
 	switch {
 	case sqlx.Has(from.Attrs, &WithoutRowID{}) && !sqlx.Has(to.Attrs, &WithoutRowID{}):
@@ -40,7 +40,7 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 }
 
 // ColumnChange returns the schema changes (if any) for migrating one column to the other.
-func (d *diff) ColumnChange(_ *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
+func (d *Diff) ColumnChange(_ *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
 	change := sqlx.CommentChange(from.Attrs, to.Attrs)
 	if from.Type.Null != to.Type.Null {
 		change |= schema.ChangeNull
@@ -62,7 +62,7 @@ func (d *diff) ColumnChange(_ *schema.Table, from, to *schema.Column) (schema.Ch
 }
 
 // typeChanged reports if the column type was changed.
-func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
+func (d *Diff) typeChanged(from, to *schema.Column) (bool, error) {
 	fromT, toT := from.Type.Type, to.Type.Type
 	if fromT == nil || toT == nil {
 		return false, fmt.Errorf("sqlite: missing type information for column %q", from.Name)
@@ -72,7 +72,7 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 }
 
 // defaultChanged reports if the default value of a column was changed.
-func (d *diff) defaultChanged(from, to *schema.Column) bool {
+func (d *Diff) defaultChanged(from, to *schema.Column) bool {
 	d1, ok1 := sqlx.DefaultValue(from)
 	d2, ok2 := sqlx.DefaultValue(to)
 	if ok1 != ok2 {
@@ -87,7 +87,7 @@ func (d *diff) defaultChanged(from, to *schema.Column) bool {
 }
 
 // generatedChanged reports if the generated expression of a column was changed.
-func (*diff) generatedChanged(from, to *schema.Column) bool {
+func (*Diff) generatedChanged(from, to *schema.Column) bool {
 	var (
 		fromX, toX     schema.GeneratedExpr
 		fromHas, toHas = sqlx.Has(from.Attrs, &fromX), sqlx.Has(to.Attrs, &toX)
@@ -97,7 +97,7 @@ func (*diff) generatedChanged(from, to *schema.Column) bool {
 
 // IsGeneratedIndexName reports if the index name was generated by the database.
 // See: https://github.com/sqlite/sqlite/blob/e937df8/src/build.c#L3583.
-func (d *diff) IsGeneratedIndexName(t *schema.Table, idx *schema.Index) bool {
+func (d *Diff) IsGeneratedIndexName(t *schema.Table, idx *schema.Index) bool {
 	p := fmt.Sprintf("sqlite_autoindex_%s_", t.Name)
 	if !strings.HasPrefix(idx.Name, p) {
 		return false
@@ -107,18 +107,18 @@ func (d *diff) IsGeneratedIndexName(t *schema.Table, idx *schema.Index) bool {
 }
 
 // IndexAttrChanged reports if the index attributes were changed.
-func (*diff) IndexAttrChanged(from, to []schema.Attr) bool {
+func (*Diff) IndexAttrChanged(from, to []schema.Attr) bool {
 	var p1, p2 IndexPredicate
 	return sqlx.Has(from, &p1) != sqlx.Has(to, &p2) || (p1.P != p2.P && p1.P != sqlx.MayWrap(p2.P))
 }
 
 // IndexPartAttrChanged reports if the index-part attributes were changed.
-func (*diff) IndexPartAttrChanged(_, _ *schema.IndexPart) bool {
+func (*Diff) IndexPartAttrChanged(_, _ *schema.IndexPart) bool {
 	return false
 }
 
 // ReferenceChanged reports if the foreign key referential action was changed.
-func (*diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
+func (*Diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
 	// According to SQLite, if an action is not explicitly
 	// specified, it defaults to "NO ACTION".
 	if from == "" {
@@ -131,7 +131,7 @@ func (*diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
 }
 
 // Normalize implements the sqlx.Normalizer interface.
-func (d *diff) Normalize(from, to *schema.Table) error {
+func (d *Diff) Normalize(from, to *schema.Table) error {
 	used := make([]bool, len(to.ForeignKeys))
 	// In SQLite, there is no easy way to get the foreign-key constraint
 	// name, except for parsing the CREATE statement). Therefore, we check
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index 111c175f..5c09b281 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -42,9 +42,12 @@ type (
 	}
 )
 
+// DriverName holds the name used for registration.
+const DriverName = "sqlite3"
+
 func init() {
 	sqlclient.Register(
-		"sqlite3",
+		DriverName,
 		sqlclient.DriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterFlavours("sqlite"),
@@ -80,7 +83,7 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 	}
 	return &Driver{
 		conn:        c,
-		Differ:      &sqlx.Diff{DiffDriver: &diff{c}},
+		Differ:      &sqlx.Diff{DiffDriver: &Diff{}},
 		Inspector:   &inspect{c},
 		PlanApplier: &planApply{c},
 	}, nil
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck.go b/sql/sqlite/sqlitecheck/sqlitecheck.go
new file mode 100644
index 00000000..9075c5e5
--- /dev/null
+++ b/sql/sqlite/sqlitecheck/sqlitecheck.go
@@ -0,0 +1,83 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package sqlitecheck
+
+import (
+	"context"
+	"strings"
+
+	"ariga.io/atlas/schemahcl"
+	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlcheck"
+	"ariga.io/atlas/sql/sqlcheck/datadepend"
+	"ariga.io/atlas/sql/sqlcheck/destructive"
+	"ariga.io/atlas/sql/sqlite"
+)
+
+func init() {
+	sqlcheck.Register(sqlite.DriverName, func(*schemahcl.Resource) (sqlcheck.Analyzer, error) {
+		return sqlcheck.Analyzers{
+			sqlcheck.AnalyzerFunc(func(ctx context.Context, p *sqlcheck.Pass) error {
+				var changes []*sqlcheck.Change
+				// Detect sequence of changes using temporary table and transform them to one ModifyTable change.
+				// See: https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes.
+				for i := 0; i < len(p.File.Changes); i++ {
+					if i+3 >= len(p.File.Changes) || !modifyUsingTemp(p.File.Changes[i], p.File.Changes[i+2], p.File.Changes[i+3]) {
+						changes = append(changes, p.File.Changes[i])
+						continue
+					}
+					prevT, currT := p.File.Changes[i+2].Changes[0].(*schema.DropTable).T, p.File.Changes[i+3].Changes[1].(*schema.AddTable).T
+					diff, err := p.Dev.Driver.TableDiff(prevT, currT)
+					if err != nil {
+						return nil
+					}
+					changes = append(changes, &sqlcheck.Change{
+						// Use the position of the first statement.
+						Pos: p.File.Changes[i].Pos,
+						// A combined statement.
+						Stmt: strings.Join([]string{
+							p.File.Changes[i].Stmt,
+							p.File.Changes[i+2].Stmt,
+							p.File.Changes[i+3].Stmt,
+						}, "\n"),
+						Changes: schema.Changes{
+							&schema.ModifyTable{
+								T:       currT,
+								Changes: diff,
+							},
+						},
+					})
+					i += 3
+				}
+				p.File.Changes = changes
+				return nil
+			}),
+			destructive.New(destructive.Options{}),
+			datadepend.New(datadepend.Options{}),
+		}, nil
+	})
+}
+
+// modifyUsingTemp indicates if the 3 changes represents a table modification using
+// the pattern mentioned in the link below: "CREATE", "INSERT", "DROP" and "RENAME".
+func modifyUsingTemp(c1, c2, c3 *sqlcheck.Change) bool {
+	if len(c1.Changes) != 1 || !isAddT(c1.Changes[0], "new_") || len(c2.Changes) != 1 || len(c3.Changes) != 2 {
+		return false
+	}
+	add := c1.Changes[0].(*schema.AddTable)
+	name := strings.TrimPrefix(add.T.Name, "new_")
+	// "DROP T" and "RENAME new_T to T".
+	return isDropT(c2.Changes[0], name) && isDropT(c3.Changes[0], add.T.Name) && isAddT(c3.Changes[1], name)
+}
+
+func isAddT(c schema.Change, prefix string) bool {
+	a, ok := c.(*schema.AddTable)
+	return ok && strings.HasPrefix(a.T.Name, prefix)
+}
+
+func isDropT(c schema.Change, prefix string) bool {
+	d, ok := c.(*schema.DropTable)
+	return ok && strings.HasPrefix(d.T.Name, prefix)
+}


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index 7489add6..98f84bcf 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -23,6 +23,7 @@ import (
 	"ariga.io/atlas/sql/sqlclient"
 	"ariga.io/atlas/sql/sqlite"
 	_ "ariga.io/atlas/sql/sqlite"
+	_ "ariga.io/atlas/sql/sqlite/sqlitecheck"
 
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/require"
diff --git a/sql/mysql/internal/mysqlcheck/datadepend_test.go b/sql/mysql/mysqlcheck/mysqlcheck_test.go
similarity index 93%
rename from sql/mysql/internal/mysqlcheck/datadepend_test.go
rename to sql/mysql/mysqlcheck/mysqlcheck_test.go
index 971f860e..f8671513 100644
--- a/sql/mysql/internal/mysqlcheck/datadepend_test.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck_test.go
@@ -10,19 +10,14 @@ import (
 
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/mysql"
-	"ariga.io/atlas/sql/mysql/internal/mysqlcheck"
+	"ariga.io/atlas/sql/mysql/mysqlcheck"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
-	"ariga.io/atlas/sql/sqlcheck/datadepend"
 	"ariga.io/atlas/sql/sqlclient"
 
 	"github.com/stretchr/testify/require"
 )
 
-func init() {
-	datadepend.Register("mysql", mysqlcheck.RegisterDataDepend)
-}
-
 func TestDataDepend_MySQL_ImplicitUpdate(t *testing.T) {
 	var (
 		report *sqlcheck.Report
@@ -65,9 +60,9 @@ func TestDataDepend_MySQL_ImplicitUpdate(t *testing.T) {
 			}),
 		}
 	)
-	az := datadepend.New(datadepend.Options{})
-	err := az.Analyze(context.Background(), pass)
+	az, err := mysqlcheck.NewDataDepend(nil)
 	require.NoError(t, err)
+	require.NoError(t, az.Analyze(context.Background(), pass))
 	require.Equal(t, report.Diagnostics[0].Text, `Adding a non-nullable "int" column "b" on table "users" without a default value implicitly sets existing rows with 0`)
 	require.Equal(t, report.Diagnostics[1].Text, `Adding a non-nullable "float" column "c" on table "users" without a default value implicitly sets existing rows with 0`)
 	require.Equal(t, report.Diagnostics[2].Text, `Adding a non-nullable "varchar" column "d" on table "users" without a default value implicitly sets existing rows with ""`)
@@ -111,9 +106,9 @@ func TestDataDepend_MySQL8_ImplicitUpdate(t *testing.T) {
 			}),
 		}
 	)
-	az := datadepend.New(datadepend.Options{})
-	err := az.Analyze(context.Background(), pass)
+	az, err := mysqlcheck.NewDataDepend(nil)
 	require.NoError(t, err)
+	require.NoError(t, az.Analyze(context.Background(), pass))
 	require.Equal(t,
 		report.Diagnostics[0].Text,
 		`Adding a non-nullable "timestamp" column "b" on table "users" without a default value implicitly sets existing rows with 0000-00-00 00:00:00`,
@@ -159,10 +154,9 @@ func TestDataDepend_MySQL_MightFail(t *testing.T) {
 			}),
 		}
 	)
-	datadepend.Register("mysql", mysqlcheck.RegisterDataDepend)
-	az := datadepend.New(datadepend.Options{})
-	err := az.Analyze(context.Background(), pass)
+	az, err := mysqlcheck.NewDataDepend(nil)
 	require.NoError(t, err)
+	require.NoError(t, az.Analyze(context.Background(), pass))
 	require.Equal(t, report.Diagnostics[0].Text, `Adding a non-nullable "date" column "b" will fail in case table "users" is not empty`)
 	require.Equal(t, report.Diagnostics[1].Text, `Adding a non-nullable "datetime" column "c" will fail in case table "users" is not empty`)
 	require.Equal(t, report.Diagnostics[2].Text, `Adding a non-nullable "point" column "d" will fail in case table "users" is not empty`)
@@ -216,9 +210,9 @@ func TestDataDepend_Maria_ImplicitUpdate(t *testing.T) {
 			}),
 		}
 	)
-	az := datadepend.New(datadepend.Options{})
-	err := az.Analyze(context.Background(), pass)
+	az, err := mysqlcheck.NewDataDepend(nil)
 	require.NoError(t, err)
+	require.NoError(t, az.Analyze(context.Background(), pass))
 	require.Equal(t, report.Diagnostics[0].Text, `Adding a non-nullable "text" column "b" on table "users" without a default value implicitly sets existing rows with ""`)
 	require.Equal(t, report.Diagnostics[1].Text, `Adding a non-nullable "json" column "c" on table "users" without a default value implicitly sets existing rows with ""`)
 	require.Equal(t, report.Diagnostics[2].Text, `Adding a non-nullable "date" column "d" on table "users" without a default value implicitly sets existing rows with 00:00:00`)
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck_test.go b/sql/sqlite/sqlitecheck/sqlitecheck_test.go
new file mode 100644
index 00000000..16b6e0b3
--- /dev/null
+++ b/sql/sqlite/sqlitecheck/sqlitecheck_test.go
@@ -0,0 +1,129 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package sqlitecheck_test
+
+import (
+	"context"
+	"testing"
+
+	"ariga.io/atlas/sql/internal/sqlx"
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlcheck"
+	"ariga.io/atlas/sql/sqlclient"
+	"ariga.io/atlas/sql/sqlite"
+	_ "ariga.io/atlas/sql/sqlite/sqlitecheck"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDetectModifyTable(t *testing.T) {
+	var (
+		report *sqlcheck.Report
+		pass   = &sqlcheck.Pass{
+			Dev: &sqlclient.Client{
+				Driver: func() migrate.Driver {
+					drv := &sqlite.Driver{}
+					drv.Differ = &sqlx.Diff{DiffDriver: &sqlite.Diff{}}
+					return drv
+				}(),
+			},
+			File: &sqlcheck.File{
+				File: testFile{name: "1.sql"},
+				Changes: []*sqlcheck.Change{
+					// A real drop.
+					{
+						Stmt: "DROP TABLE `users`",
+						Changes: schema.Changes{
+							&schema.DropTable{
+								T: schema.NewTable("users").
+									SetSchema(schema.New("main")),
+							},
+						},
+					},
+					// Table modification using a temporary table.
+					{
+						Stmt: "PRAGMA foreign_keys = off;",
+					},
+					{
+						Stmt: "CREATE TABLE `new_posts` (`text` text NOT NULL);",
+						Changes: schema.Changes{
+							&schema.AddTable{
+								T: schema.NewTable("new_posts").
+									SetSchema(schema.New("main")).
+									AddColumns(schema.NewStringColumn("text", "text")),
+							},
+						},
+					},
+					{
+						Stmt: "INSERT INTO `new_posts` (`text`) SELECT `text` FROM `posts`;",
+					},
+					{
+						Stmt: "DROP TABLE `posts`",
+						Changes: schema.Changes{
+							&schema.DropTable{
+								T: schema.NewTable("posts").
+									SetSchema(schema.New("main")).
+									AddColumns(
+										schema.NewStringColumn("text", "text"),
+										schema.NewTimeColumn("posted_at", "timestamp"),
+									),
+							},
+						},
+					},
+					{
+						Stmt: "ALTER TABLE `new_posts` RENAME TO `posts`;",
+						Changes: schema.Changes{
+							&schema.DropTable{
+								T: schema.NewTable("new_posts").
+									SetSchema(schema.New("main")).
+									AddColumns(schema.NewStringColumn("text", "text")),
+							},
+							&schema.AddTable{
+								T: schema.NewTable("posts").
+									SetSchema(schema.New("main")).
+									AddColumns(schema.NewStringColumn("text", "text")),
+							},
+						},
+					},
+					{
+						Stmt: "PRAGMA foreign_keys = on;",
+					},
+					// Another real drop.
+					{
+						Stmt: "DROP TABLE `pets`",
+						Changes: schema.Changes{
+							&schema.DropTable{
+								T: schema.NewTable("pets").
+									SetSchema(schema.New("main")),
+							},
+						},
+					},
+				},
+			},
+			Reporter: sqlcheck.ReportWriterFunc(func(r sqlcheck.Report) {
+				report = &r
+			}),
+		}
+	)
+	az, err := sqlcheck.AnalyzerFor(sqlite.DriverName, nil)
+	require.NoError(t, err)
+	require.NoError(t, az.Analyze(context.Background(), pass))
+
+	require.Equal(t, report.Text, "Destructive changes detected in file 1.sql")
+	require.Len(t, report.Diagnostics, 3)
+	require.Equal(t, report.Diagnostics[0].Text, `Dropping table "users"`)
+	require.Equal(t, report.Diagnostics[1].Text, `Dropping non-virtual column "posted_at"`)
+	require.Equal(t, report.Diagnostics[2].Text, `Dropping table "pets"`)
+}
+
+type testFile struct {
+	name string
+	migrate.File
+}
+
+func (t testFile) Name() string {
+	return t.name
+}