Repository: ariga/atlas
Pull request #935 (2022-07-11 13:45:18)

cmd/atlas/internal/cmdapi: create migration directory on atlas migrat…
…e diff command if it does not exist yet


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..01b2c508 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -10,6 +10,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -23,8 +24,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"
@@ -84,20 +83,12 @@ var (
 			}
 			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
 			if !MigrateFlags.Force {
-				dir, err := dir()
+				dir, err := dir(false)
 				if err != nil {
 					return err
 				}
 				if err := migrate.Validate(dir); err != nil {
-					fmt.Fprintf(cmd.OutOrStderr(), `You have a checksum error in your migration directory.
-This happens if you manually create or edit a migration file.
-Please check your migration files and run
-
-'atlas migrate hash --force'
-
-to re-hash the contents and resolve the error
-
-`)
+					printChecksumErr(cmd.OutOrStderr())
 					cmd.SilenceUsage = true
 					return err
 				}
@@ -121,9 +112,8 @@ If run with the "--dry-run" flag, atlas will not execute any SQL.`,
   atlas migrate apply --dir file:///path/to/migration/directory --url mysql://user:pass@localhost:3306/dbname 1
   atlas migrate apply --env dev 1
   atlas migrate apply --dry-run --env dev 1`,
-		Args:    cobra.MaximumNArgs(1),
-		PreRunE: migrateFlagsFromEnv,
-		RunE:    CmdMigrateApplyRun,
+		Args: cobra.MaximumNArgs(1),
+		RunE: CmdMigrateApplyRun,
 	}
 	// MigrateDiffCmd represents the 'atlas migrate diff' subcommand.
 	MigrateDiffCmd = &cobra.Command{
@@ -136,9 +126,28 @@ directory state to the desired schema. The desired state can be another connecte
   atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to file://atlas.hcl add_users_table
   atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to mysql://user:pass@localhost:3306/dbname
   atlas migrate diff --env dev`,
-		Args:    cobra.MaximumNArgs(1),
-		PreRunE: migrateFlagsFromEnv,
-		RunE:    CmdMigrateDiffRun,
+		Args: cobra.MaximumNArgs(1),
+		// If the migration directory does not exist on the validation attempt, this command will create it and
+		// consider the new migration directory "valid".
+		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+			if err := migrateFlagsFromEnv(cmd, nil); err != nil {
+				return err
+			}
+			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
+			if !MigrateFlags.Force {
+				dir, err := dir(true)
+				if err != nil {
+					return err
+				}
+				if err := migrate.Validate(dir); err != nil {
+					printChecksumErr(cmd.OutOrStderr())
+					cmd.SilenceUsage = true
+					return err
+				}
+			}
+			return nil
+		},
+		RunE: CmdMigrateDiffRun,
 	}
 	// MigrateHashCmd represents the 'atlas migrate hash' command.
 	MigrateHashCmd = &cobra.Command{
@@ -147,7 +156,6 @@ directory state to the desired schema. The desired state can be another connecte
 		Long: `'atlas migrate hash' computes the integrity hash sum of the migration directory and stores it in the atlas.sum file.
 This command should be used whenever a manual change in the migration directory was made.`,
 		Example: `  atlas migrate hash --force`,
-		PreRunE: migrateFlagsFromEnv,
 		RunE:    CmdMigrateHashRun,
 	}
 	// MigrateNewCmd represents the 'atlas migrate new' command.
@@ -157,7 +165,6 @@ This command should be used whenever a manual change in the migration directory
 		Long:    `'atlas migrate new' creates a new migration according to the configured formatter without any statements in it.`,
 		Example: `  atlas migrate new my-new-migration`,
 		Args:    cobra.MaximumNArgs(1),
-		PreRunE: migrateFlagsFromEnv,
 		RunE:    CmdMigrateNewRun,
 	}
 	// MigrateStatusCmd represents the 'atlas migrate status' command.
@@ -167,8 +174,7 @@ This command should be used whenever a manual change in the migration directory
 		Long:  `'atlas migrate status' reports information about the current status of a connected database compared to the migration directory.`,
 		Example: `  atlas migrate status --url mysql://user:pass@localhost:3306/
   atlas migrate status --url mysql://user:pass@localhost:3306/ --dir file:///path/to/migration/directory`,
-		PreRunE: migrateFlagsFromEnv,
-		RunE:    CmdMigrateStatusRun,
+		RunE: CmdMigrateStatusRun,
 	}
 	// MigrateValidateCmd represents the 'atlas migrate validate' command.
 	MigrateValidateCmd = &cobra.Command{
@@ -181,8 +187,7 @@ files are executed on the connected database in order to validate SQL semantics.
   atlas migrate validate --dir file:///path/to/migration/directory
   atlas migrate validate --dir file:///path/to/migration/directory --dev-url mysql://user:pass@localhost:3306/dev
   atlas migrate validate --env dev`,
-		PreRunE: migrateFlagsFromEnv,
-		RunE:    CmdMigrateValidateRun,
+		RunE: CmdMigrateValidateRun,
 	}
 	// MigrateLintCmd represents the 'atlas migrate Lint' command.
 	MigrateLintCmd = &cobra.Command{
@@ -259,7 +264,7 @@ func CmdMigrateApplyRun(cmd *cobra.Command, args []string) error {
 		}
 	}
 	// Open the migration directory.
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -347,7 +352,7 @@ func CmdMigrateDiffRun(cmd *cobra.Command, args []string) error {
 		defer cobra.CheckErr(unlock())
 	}
 	// Open the migration directory.
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -383,7 +388,7 @@ func CmdMigrateDiffRun(cmd *cobra.Command, args []string) error {
 
 // CmdMigrateHashRun is the command executed when running the CLI with 'migrate hash' args.
 func CmdMigrateHashRun(*cobra.Command, []string) error {
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -396,7 +401,7 @@ func CmdMigrateHashRun(*cobra.Command, []string) error {
 
 // CmdMigrateNewRun is the command executed when running the CLI with 'migrate new' args.
 func CmdMigrateNewRun(_ *cobra.Command, args []string) error {
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -414,7 +419,7 @@ func CmdMigrateNewRun(_ *cobra.Command, args []string) error {
 // CmdMigrateStatusRun is the command executed when running the CLI with 'migrate status' args.
 func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
 	// Open the migration directory.
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -525,7 +530,7 @@ func CmdMigrateValidateRun(cmd *cobra.Command, _ []string) error {
 	}
 	defer dev.Close()
 	// Currently, only our own migration file format is supported.
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -546,7 +551,7 @@ func CmdMigrateLintRun(cmd *cobra.Command, _ []string) error {
 		return err
 	}
 	defer dev.Close()
-	dir, err := dir()
+	dir, err := dir(false)
 	if err != nil {
 		return err
 	}
@@ -579,6 +584,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,14 +596,23 @@ 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())
 }
 
+func printChecksumErr(out io.Writer) {
+	fmt.Fprintf(out, `You have a checksum error in your migration directory.
+This happens if you manually create or edit a migration file.
+Please check your migration files and run
+
+'atlas migrate hash --force'
+
+to re-hash the contents and resolve the error
+
+`)
+}
+
 func revisionsTableExists(ctx context.Context, c *sqlclient.Client) (bool, error) {
 	// Connect to the given schema name.
 	sc, err := sqlclient.Open(ctx, MigrateFlags.URL, sqlclient.OpenSchema(MigrateFlags.RevisionSchema))
@@ -626,7 +644,7 @@ func revisionsTableExists(ctx context.Context, c *sqlclient.Client) (bool, error
 }
 
 // dir returns a migrate.Dir to use as migration directory. For now only local directories are supported.
-func dir() (migrate.Dir, error) {
+func dir(create bool) (migrate.Dir, error) {
 	parts := strings.SplitN(MigrateFlags.DirURL, "://", 2)
 	if len(parts) != 2 {
 		return nil, fmt.Errorf("invalid dir url %q", MigrateFlags.DirURL)
@@ -634,7 +652,14 @@ func dir() (migrate.Dir, error) {
 	if parts[0] != "file" {
 		return nil, fmt.Errorf("unsupported driver %q", parts[0])
 	}
-	return migrate.NewLocalDir(parts[1])
+	d, err := migrate.NewLocalDir(parts[1])
+	if create && errors.Is(err, fs.ErrNotExist) {
+		if err := os.MkdirAll(parts[1], 0755); err != nil {
+			return nil, err
+		}
+		d, err = migrate.NewLocalDir(parts[1])
+	}
+	return d, err
 }
 
 // to returns a migrate.StateReader for the given to flag.
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..2e59e84b 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"
@@ -143,7 +144,18 @@ func TestMigrate_Apply(t *testing.T) {
 func TestMigrate_Diff(t *testing.T) {
 	p := t.TempDir()
 
+	// Will create migration directory if not existing.
+	_, err := runCmd(
+		Root, "migrate", "diff",
+		"name",
+		"--dir", "file://"+filepath.Join(p, "migrations"),
+		"--dev-url", openSQLite(t, ""),
+		"--to", hclURL(t))
+	require.NoError(t, err)
+	require.FileExists(t, filepath.Join(p, "migrations", fmt.Sprintf("%s_name.sql", time.Now().UTC().Format("20060102150405"))))
+
 	// Expect no clean dev error.
+	p = t.TempDir()
 	s, err := runCmd(
 		Root, "migrate", "diff",
 		"name",
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
+}