Repository: ariga/atlas
Pull request #1092 (2022-08-23 18:29:39)

doc/website/blog: move sentence to right place


Patch:
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index 517a9a05..16f3da23 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -138,6 +138,7 @@ func init() {
 	SchemaApply.Flags().SortFlags = false
 	SchemaApply.Flags().StringSliceVarP(&ApplyFlags.Paths, fileFlag, "f", nil, "[paths...] file or directory containing the HCL files")
 	SchemaApply.Flags().StringVarP(&SchemaFlags.URL, urlFlag, "u", "", "URL to the database using the format:\n[driver://username:password@address/dbname?param=value]")
+	SchemaApply.Flags().StringSliceVarP(&SchemaFlags.Exclude, excludeFlag, "", nil, "List of glob patterns used to filter resources from applying.")
 	SchemaApply.Flags().StringSliceVarP(&SchemaFlags.Schemas, schemaFlag, "s", nil, "Set schema names.")
 	SchemaApply.Flags().StringVarP(&ApplyFlags.DevURL, devURLFlag, "", "", "URL for the dev database. Used to validate schemas and calculate diffs\nbefore running migration.")
 	SchemaApply.Flags().BoolVarP(&ApplyFlags.DryRun, "dry-run", "", false, "Dry-run. Print SQL plan without prompting for execution.")
diff --git a/cmd/atlas/internal/lint/run.go b/cmd/atlas/internal/lint/run.go
index a93bf250..d07c89d5 100644
--- a/cmd/atlas/internal/lint/run.go
+++ b/cmd/atlas/internal/lint/run.go
@@ -145,7 +145,7 @@ var (
 	{{- else }}
 		{{- range $r := $f.Reports }}
 			{{- if $r.Text }}
-				{{- printf "%s:\n\n" $r.Text }}
+				{{- printf "%s: %s:\n\n" $f.Name $r.Text }}
 			{{- else if $r.Diagnostics }}
 				{{- printf "Unnamed diagnostics for file %s:\n\n" $f.Name }}
 			{{- end }}
diff --git a/cmd/atlas/internal/migrate/ent/migrate/schema.go b/cmd/atlas/internal/migrate/ent/migrate/schema.go
index e2342762..470df65a 100644
--- a/cmd/atlas/internal/migrate/ent/migrate/schema.go
+++ b/cmd/atlas/internal/migrate/ent/migrate/schema.go
@@ -17,7 +17,7 @@ var (
 	AtlasSchemaRevisionsColumns = []*schema.Column{
 		{Name: "version", Type: field.TypeString},
 		{Name: "description", Type: field.TypeString},
-		{Name: "type", Type: field.TypeUint},
+		{Name: "type", Type: field.TypeUint, Default: 2},
 		{Name: "applied", Type: field.TypeInt},
 		{Name: "total", Type: field.TypeInt},
 		{Name: "executed_at", Type: field.TypeTime},
diff --git a/cmd/atlas/internal/migrate/ent/revision/revision.go b/cmd/atlas/internal/migrate/ent/revision/revision.go
index 6361a113..7598cfd0 100644
--- a/cmd/atlas/internal/migrate/ent/revision/revision.go
+++ b/cmd/atlas/internal/migrate/ent/revision/revision.go
@@ -6,6 +6,10 @@
 
 package revision
 
+import (
+	"ariga.io/atlas/sql/migrate"
+)
+
 const (
 	// Label holds the string label denoting the revision type in the database.
 	Label = "revision"
@@ -61,6 +65,8 @@ func ValidColumn(column string) bool {
 }
 
 var (
+	// DefaultType holds the default value on creation for the "type" field.
+	DefaultType migrate.RevisionType
 	// AppliedValidator is a validator for the "applied" field. It is called by the builders before save.
 	AppliedValidator func(int) error
 	// TotalValidator is a validator for the "total" field. It is called by the builders before save.
diff --git a/cmd/atlas/internal/migrate/ent/revision_create.go b/cmd/atlas/internal/migrate/ent/revision_create.go
index c40dbaca..8adc9c09 100644
--- a/cmd/atlas/internal/migrate/ent/revision_create.go
+++ b/cmd/atlas/internal/migrate/ent/revision_create.go
@@ -40,6 +40,14 @@ func (rc *RevisionCreate) SetType(mt migrate.RevisionType) *RevisionCreate {
 	return rc
 }
 
+// SetNillableType sets the "type" field if the given value is not nil.
+func (rc *RevisionCreate) SetNillableType(mt *migrate.RevisionType) *RevisionCreate {
+	if mt != nil {
+		rc.SetType(*mt)
+	}
+	return rc
+}
+
 // SetApplied sets the "applied" field.
 func (rc *RevisionCreate) SetApplied(i int) *RevisionCreate {
 	rc.mutation.SetApplied(i)
@@ -113,6 +121,7 @@ func (rc *RevisionCreate) Save(ctx context.Context) (*Revision, error) {
 		err  error
 		node *Revision
 	)
+	rc.defaults()
 	if len(rc.hooks) == 0 {
 		if err = rc.check(); err != nil {
 			return nil, err
@@ -176,6 +185,14 @@ func (rc *RevisionCreate) ExecX(ctx context.Context) {
 	}
 }
 
+// defaults sets the default values of the builder before save.
+func (rc *RevisionCreate) defaults() {
+	if _, ok := rc.mutation.GetType(); !ok {
+		v := revision.DefaultType
+		rc.mutation.SetType(v)
+	}
+}
+
 // check runs all checks and user-defined validators on the builder.
 func (rc *RevisionCreate) check() error {
 	if _, ok := rc.mutation.Description(); !ok {
@@ -827,6 +844,7 @@ func (rcb *RevisionCreateBulk) Save(ctx context.Context) ([]*Revision, error) {
 	for i := range rcb.builders {
 		func(i int, root context.Context) {
 			builder := rcb.builders[i]
+			builder.defaults()
 			var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
 				mutation, ok := m.(*RevisionMutation)
 				if !ok {
diff --git a/cmd/atlas/internal/migrate/ent/revision_update.go b/cmd/atlas/internal/migrate/ent/revision_update.go
index 5aa52c0f..578f6559 100644
--- a/cmd/atlas/internal/migrate/ent/revision_update.go
+++ b/cmd/atlas/internal/migrate/ent/revision_update.go
@@ -41,6 +41,14 @@ func (ru *RevisionUpdate) SetType(mt migrate.RevisionType) *RevisionUpdate {
 	return ru
 }
 
+// SetNillableType sets the "type" field if the given value is not nil.
+func (ru *RevisionUpdate) SetNillableType(mt *migrate.RevisionType) *RevisionUpdate {
+	if mt != nil {
+		ru.SetType(*mt)
+	}
+	return ru
+}
+
 // AddType adds mt to the "type" field.
 func (ru *RevisionUpdate) AddType(mt migrate.RevisionType) *RevisionUpdate {
 	ru.mutation.AddType(mt)
@@ -352,6 +360,14 @@ func (ruo *RevisionUpdateOne) SetType(mt migrate.RevisionType) *RevisionUpdateOn
 	return ruo
 }
 
+// SetNillableType sets the "type" field if the given value is not nil.
+func (ruo *RevisionUpdateOne) SetNillableType(mt *migrate.RevisionType) *RevisionUpdateOne {
+	if mt != nil {
+		ruo.SetType(*mt)
+	}
+	return ruo
+}
+
 // AddType adds mt to the "type" field.
 func (ruo *RevisionUpdateOne) AddType(mt migrate.RevisionType) *RevisionUpdateOne {
 	ruo.mutation.AddType(mt)
diff --git a/cmd/atlas/internal/migrate/ent/runtime.go b/cmd/atlas/internal/migrate/ent/runtime.go
index ac51c74a..98359fb3 100644
--- a/cmd/atlas/internal/migrate/ent/runtime.go
+++ b/cmd/atlas/internal/migrate/ent/runtime.go
@@ -9,6 +9,7 @@ package ent
 import (
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/schema"
+	"ariga.io/atlas/sql/migrate"
 )
 
 // The init function reads all schema descriptors with runtime code
@@ -17,6 +18,10 @@ import (
 func init() {
 	revisionFields := schema.Revision{}.Fields()
 	_ = revisionFields
+	// revisionDescType is the schema descriptor for type field.
+	revisionDescType := revisionFields[2].Descriptor()
+	// revision.DefaultType holds the default value on creation for the type field.
+	revision.DefaultType = migrate.RevisionType(revisionDescType.Default.(uint))
 	// revisionDescApplied is the schema descriptor for applied field.
 	revisionDescApplied := revisionFields[3].Descriptor()
 	// revision.AppliedValidator is a validator for the "applied" field. It is called by the builders before save.
diff --git a/cmd/atlas/internal/migrate/ent/schema/revision.go b/cmd/atlas/internal/migrate/ent/schema/revision.go
index e4fe6f92..7075fbe5 100644
--- a/cmd/atlas/internal/migrate/ent/schema/revision.go
+++ b/cmd/atlas/internal/migrate/ent/schema/revision.go
@@ -29,7 +29,8 @@ func (Revision) Fields() []ent.Field {
 		field.String("description").
 			Immutable(),
 		field.Uint("type").
-			GoType(migrate.RevisionType(0)),
+			GoType(migrate.RevisionType(0)).
+			Default(uint(migrate.RevisionTypeExecute)),
 		field.Int("applied").
 			NonNegative(),
 		field.Int("total").
diff --git a/cmd/atlas/internal/migrate/migrate.go b/cmd/atlas/internal/migrate/migrate.go
index d5d2ae9e..7cbeb19d 100644
--- a/cmd/atlas/internal/migrate/migrate.go
+++ b/cmd/atlas/internal/migrate/migrate.go
@@ -15,6 +15,7 @@ import (
 	"ariga.io/atlas/sql/sqlclient"
 	"entgo.io/ent/dialect"
 	"entgo.io/ent/dialect/sql"
+	entschema "entgo.io/ent/dialect/sql/schema"
 )
 
 // DefaultRevisionSchema is the default schema for storing revisions table.
@@ -91,7 +92,7 @@ func (r *EntRevisions) Init(ctx context.Context) error {
 	}
 	r.ac.AddClosers(sc)
 	r.ec = ent.NewClient(ent.Driver(sql.OpenDB(sc.Name, sc.DB)))
-	return r.ec.Schema.Create(ctx)
+	return r.ec.Schema.Create(ctx, entschema.WithDropColumn(true))
 }
 
 // ReadRevision reads a revision from the revisions table.
diff --git a/doc/md/reference.md b/doc/md/reference.md
index 18a4e761..1f2658d9 100644
--- a/doc/md/reference.md
+++ b/doc/md/reference.md
@@ -325,14 +325,15 @@ migration.
 ```
 #### Flags
 ```
-  -f, --file strings     [paths...] file or directory containing the HCL files
-  -u, --url string       URL to the database using the format:
-                         [driver://username:password@address/dbname?param=value]
-  -s, --schema strings   Set schema names.
-      --dev-url string   URL for the dev database. Used to validate schemas and calculate diffs
-                         before running migration.
-      --dry-run          Dry-run. Print SQL plan without prompting for execution.
-      --auto-approve     Auto approve. Apply the schema changes without prompting for approval.
+  -f, --file strings      [paths...] file or directory containing the HCL files
+  -u, --url string        URL to the database using the format:
+                          [driver://username:password@address/dbname?param=value]
+      --exclude strings   List of glob patterns used to filter resources from applying.
+  -s, --schema strings    Set schema names.
+      --dev-url string    URL for the dev database. Used to validate schemas and calculate diffs
+                          before running migration.
+      --dry-run           Dry-run. Print SQL plan without prompting for execution.
+      --auto-approve      Auto approve. Apply the schema changes without prompting for approval.
 
 ```
 
diff --git a/doc/website/blog/2022-08-22-atlas-ci-github-actions.md b/doc/website/blog/2022-08-22-atlas-ci-github-actions.md
index ad546924..7fb3ef92 100644
--- a/doc/website/blog/2022-08-22-atlas-ci-github-actions.md
+++ b/doc/website/blog/2022-08-22-atlas-ci-github-actions.md
@@ -159,14 +159,15 @@ Examining the Action [run summary](https://github.com/rotemtam/atlas-action-demo
 
 ![](https://atlasgo.io/uploads/images/workflow-summary-03.png)
 
+As you can see, Atlas has detected the destructive change we proposed to apply 
+to our database and failed our build!
+
 ### Wrapping up
 
 In this post we discussed why many teams set policies to prevent destructive
 changes to database schemas. We further showed how such policies can be enforced
 in an automated way using the official [Atlas GitHub Action](https://atlasgo.io/integrations/github-actions).
 
-As you can see, Atlas has detected the destructive change we proposed to apply 
-to our database and failed our build!
 
 ### Further reading
 
diff --git a/sql/internal/sqlx/sqlx.go b/sql/internal/sqlx/sqlx.go
index 34348964..59f6d4b4 100644
--- a/sql/internal/sqlx/sqlx.go
+++ b/sql/internal/sqlx/sqlx.go
@@ -447,3 +447,17 @@ func ReverseChanges(c []schema.Change) {
 		c[i], c[n-i-1] = c[n-i-1], c[i]
 	}
 }
+
+// P returns a pointer to v.
+func P[T any](v T) *T {
+	return &v
+}
+
+// V returns the value p is pointing to.
+// If p is nil, the zero value is returned.
+func V[T any](p *T) (v T) {
+	if p != nil {
+		v = *p
+	}
+	return
+}
diff --git a/sql/mysql/mysqlcheck/mysqlcheck.go b/sql/mysql/mysqlcheck/mysqlcheck.go
index 9cd7f5a9..c39c3785 100644
--- a/sql/mysql/mysqlcheck/mysqlcheck.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck.go
@@ -17,13 +17,6 @@ import (
 	"ariga.io/atlas/sql/sqlcheck/destructive"
 )
 
-// NewDataDepend creates new data-depend analyzer.
-func NewDataDepend(*schemahcl.Resource) *datadepend.Analyzer {
-	var opts datadepend.Options
-	opts.Handler.AddNotNull = addNotNull
-	return datadepend.New(opts)
-}
-
 func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err error) {
 	// Two types of reporting, implicit rows update and
 	// changes that may cause the migration to fail.
@@ -133,10 +126,16 @@ func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err erro
 
 func init() {
 	sqlcheck.Register(mysql.DriverName, func(r *schemahcl.Resource) (sqlcheck.Analyzer, error) {
-		d, err := destructive.New(r)
+		ds, err := destructive.New(r)
+		if err != nil {
+			return nil, err
+		}
+		dd, err := datadepend.New(r, datadepend.Handler{
+			AddNotNull: addNotNull,
+		})
 		if err != nil {
 			return nil, err
 		}
-		return sqlcheck.Analyzers{d, NewDataDepend(nil)}, nil
+		return sqlcheck.Analyzers{ds, dd}, nil
 	})
 }
diff --git a/sql/postgres/postgrescheck/postgrescheck.go b/sql/postgres/postgrescheck/postgrescheck.go
index ad1496f8..b9592359 100644
--- a/sql/postgres/postgrescheck/postgrescheck.go
+++ b/sql/postgres/postgrescheck/postgrescheck.go
@@ -2,7 +2,7 @@
 // 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
+package postgrescheck
 
 import (
 	"fmt"
@@ -14,33 +14,31 @@ import (
 	"ariga.io/atlas/sql/sqlcheck/destructive"
 )
 
-// NewDataDepend creates new data-depend analyzer.
-func NewDataDepend(*schemahcl.Resource) *datadepend.Analyzer {
-	var opts datadepend.Options
-	opts.Handler.AddNotNull = func(p *datadepend.ColumnPass) ([]sqlcheck.Diagnostic, error) {
-		tt, err := postgres.FormatType(p.Column.Type.Type)
-		if err != nil {
-			return nil, err
-		}
-		return []sqlcheck.Diagnostic{
-			{
-				Pos: p.Change.Pos,
-				Text: fmt.Sprintf(
-					"Adding a non-nullable %q column %q will fail in case table %q is not empty",
-					tt, p.Column.Name, p.Table.Name,
-				),
-			},
-		}, nil
+func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err error) {
+	tt, err := postgres.FormatType(p.Column.Type.Type)
+	if err != nil {
+		return nil, err
 	}
-	return datadepend.New(opts)
+	return []sqlcheck.Diagnostic{
+		{
+			Pos: p.Change.Pos,
+			Text: fmt.Sprintf(
+				"Adding a non-nullable %q column %q will fail in case table %q is not empty",
+				tt, p.Column.Name, p.Table.Name,
+			),
+		},
+	}, nil
 }
 
 func init() {
 	sqlcheck.Register(postgres.DriverName, func(r *schemahcl.Resource) (sqlcheck.Analyzer, error) {
-		d, err := destructive.New(r)
+		ds, err := destructive.New(r)
 		if err != nil {
 			return nil, err
 		}
-		return sqlcheck.Analyzers{d, NewDataDepend(nil)}, nil
+		dd, err := datadepend.New(r, datadepend.Handler{
+			AddNotNull: addNotNull,
+		})
+		return sqlcheck.Analyzers{ds, dd}, nil
 	})
 }
diff --git a/sql/sqlcheck/datadepend/datadepend.go b/sql/sqlcheck/datadepend/datadepend.go
index 6aeca413..1a5cae07 100644
--- a/sql/sqlcheck/datadepend/datadepend.go
+++ b/sql/sqlcheck/datadepend/datadepend.go
@@ -6,35 +6,27 @@ package datadepend
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
+	"ariga.io/atlas/schemahcl"
+	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
 )
 
 type (
-	// Options defines the configuration options
-	// for the data-dependent changes checker.
-	Options struct {
-		// UniqueIndex indicates if the analyzer should check for modification or
-		// addition of unique indexes to tables that can cause migration to fail.
-		UniqueIndex *bool `spec:"drop_schema,omitempty"`
-
-		// NotNull indicates if the analyzer should check for modification
-		// or addition of NOT NULL constraints to columns.
-		NotNull *bool `spec:"not_null,omitempty"`
-
-		// Underlying driver handlers.
-		Handler struct {
-			// AddNotNull is applied when a new non-nullable column was
-			// added to an existing table.
-			AddNotNull ColumnHandler
-		}
-	}
-
 	// Analyzer checks data-dependent changes.
 	Analyzer struct {
-		Options
+		sqlcheck.Options
+		Handler
+	}
+
+	// Handler holds the underlying driver handlers.
+	Handler struct {
+		// AddNotNull is applied when a new non-nullable column was
+		// added to an existing table.
+		AddNotNull ColumnHandler
 	}
 
 	// ColumnPass wraps the information needed
@@ -51,21 +43,19 @@ type (
 )
 
 // New creates a new data-dependant analyzer with the given options.
-func New(opts Options) *Analyzer {
-	notnull, unique := true, true
-	if opts.NotNull != nil {
-		notnull = *opts.NotNull
-	}
-	if opts.UniqueIndex != nil {
-		unique = *opts.UniqueIndex
+func New(r *schemahcl.Resource, h Handler) (*Analyzer, error) {
+	az := &Analyzer{Handler: h}
+	if r, ok := r.Resource("data_depend"); ok {
+		if err := r.As(&az.Options); err != nil {
+			return nil, fmt.Errorf("sql/sqlcheck: parsing datadepend check options: %w", err)
+		}
 	}
-	return &Analyzer{Options: Options{UniqueIndex: &unique, NotNull: &notnull, Handler: opts.Handler}}
+	return az, nil
 }
 
 // Analyze runs data-depend analysis on MySQL changes.
 func (a *Analyzer) Analyze(ctx context.Context, p *sqlcheck.Pass) error {
-	a.Report(p, a.Diagnostics(ctx, p))
-	return nil
+	return a.Report(p, a.Diagnostics(ctx, p))
 }
 
 // Diagnostics runs the common analysis on the file and returns its diagnostics.
@@ -90,14 +80,14 @@ func (a *Analyzer) Diagnostics(_ context.Context, p *sqlcheck.Pass) (diags []sql
 						return nil
 					}()
 					// A unique index was added on an existing column.
-					if *a.UniqueIndex && c.I.Unique && column != nil {
+					if c.I.Unique && column != nil {
 						diags = append(diags, sqlcheck.Diagnostic{
 							Pos:  sc.Pos,
 							Text: fmt.Sprintf("Adding a unique index %q on table %q might fail in case column %q contains duplicate entries", c.I.Name, m.T.Name, column.Name),
 						})
 					}
 				case *schema.ModifyIndex:
-					if *a.UniqueIndex && c.Change.Is(schema.ChangeUnique) && c.To.Unique && p.File.IndexSpan(m.T, c.To)&sqlcheck.SpanAdded == 0 {
+					if c.Change.Is(schema.ChangeUnique) && c.To.Unique && p.File.IndexSpan(m.T, c.To)&sqlcheck.SpanAdded == 0 {
 						diags = append(diags, sqlcheck.Diagnostic{
 							Pos:  sc.Pos,
 							Text: fmt.Sprintf("Modifying an index %q on table %q might fail in case of duplicate entries", c.To.Name, m.T.Name),
@@ -106,7 +96,7 @@ func (a *Analyzer) Diagnostics(_ context.Context, p *sqlcheck.Pass) (diags []sql
 				case *schema.AddColumn:
 					// In case the column is nullable without default
 					// value and the table was not added in this file.
-					if *a.NotNull && a.Handler.AddNotNull != nil && !c.C.Type.Null && c.C.Default == nil && p.File.TableSpan(m.T)&sqlcheck.SpanAdded != 1 {
+					if a.Handler.AddNotNull != nil && !c.C.Type.Null && c.C.Default == nil && p.File.TableSpan(m.T)&sqlcheck.SpanAdded != 1 {
 						d, err := a.Handler.AddNotNull(&ColumnPass{Pass: p, Change: sc, Table: m.T, Column: c.C})
 						if err != nil {
 							return
@@ -123,11 +113,23 @@ func (a *Analyzer) Diagnostics(_ context.Context, p *sqlcheck.Pass) (diags []sql
 // Report provides standard reporting for data-dependent changes. Drivers that
 // decorate this Analyzer should call this function to get consistent reporting
 // between dialects.
-func (a *Analyzer) Report(p *sqlcheck.Pass, diags []sqlcheck.Diagnostic) {
+func (a *Analyzer) Report(p *sqlcheck.Pass, diags []sqlcheck.Diagnostic) error {
 	if len(diags) > 0 {
+		text := reportText(len(diags))
 		p.Reporter.WriteReport(sqlcheck.Report{
-			Text:        fmt.Sprintf("Data dependent changes detected in file %s", p.File.Name()),
+			Text:        text,
 			Diagnostics: diags,
 		})
+		if sqlx.V(a.Error) {
+			return errors.New(text)
+		}
+	}
+	return nil
+}
+
+func reportText(n int) string {
+	if n > 1 {
+		return fmt.Sprintf("%d data dependent changes detected", n)
 	}
+	return "data dependent change detected"
 }
diff --git a/sql/sqlcheck/destructive/destructive.go b/sql/sqlcheck/destructive/destructive.go
index a6e1479e..711e3bd9 100644
--- a/sql/sqlcheck/destructive/destructive.go
+++ b/sql/sqlcheck/destructive/destructive.go
@@ -23,14 +23,14 @@ type Analyzer struct {
 
 // New creates a new destructive changes Analyzer with the given options.
 func New(r *schemahcl.Resource) (*Analyzer, error) {
-	d := &Analyzer{}
-	d.Error = true
+	az := &Analyzer{}
+	az.Error = sqlx.P(true)
 	if r, ok := r.Resource("destructive"); ok {
-		if err := r.As(&d.Options); err != nil {
+		if err := r.As(&az.Options); err != nil {
 			return nil, fmt.Errorf("sql/sqlcheck: parsing destructive check options: %w", err)
 		}
 	}
-	return d, nil
+	return az, nil
 }
 
 // Analyze implements sqlcheck.Analyzer.
@@ -76,11 +76,18 @@ func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 		}
 	}
 	if len(diags) > 0 {
-		text := fmt.Sprintf("Destructive changes detected in file %s", p.File.Name())
+		text := reportText(len(diags))
 		p.Reporter.WriteReport(sqlcheck.Report{Text: text, Diagnostics: diags})
-		if a.Error {
+		if sqlx.V(a.Error) {
 			return errors.New(text)
 		}
 	}
 	return nil
 }
+
+func reportText(n int) string {
+	if n > 1 {
+		return fmt.Sprintf("%d destructive changes detected", n)
+	}
+	return "destructive change detected"
+}
diff --git a/sql/sqlcheck/sqlcheck.go b/sql/sqlcheck/sqlcheck.go
index 5cf08bb1..11be0fdc 100644
--- a/sql/sqlcheck/sqlcheck.go
+++ b/sql/sqlcheck/sqlcheck.go
@@ -84,7 +84,7 @@ type (
 	Options struct {
 		// Error indicates if an analyzer should
 		// error in case a Diagnostic was found.
-		Error bool `spec:"error"`
+		Error *bool `spec:"error"`
 
 		// Allow drivers to extend the configuration.
 		schemahcl.DefaultExtension
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck.go b/sql/sqlite/sqlitecheck/sqlitecheck.go
index 00d4cd71..48fc7ecc 100644
--- a/sql/sqlite/sqlitecheck/sqlitecheck.go
+++ b/sql/sqlite/sqlitecheck/sqlitecheck.go
@@ -17,30 +17,31 @@ import (
 	"ariga.io/atlas/sql/sqlite"
 )
 
-// NewDataDepend creates new data-depend analyzer.
-func NewDataDepend(*schemahcl.Resource) *datadepend.Analyzer {
-	var opts datadepend.Options
-	opts.Handler.AddNotNull = func(p *datadepend.ColumnPass) ([]sqlcheck.Diagnostic, error) {
-		tt, err := sqlite.FormatType(p.Column.Type.Type)
-		if err != nil {
-			return nil, err
-		}
-		return []sqlcheck.Diagnostic{
-			{
-				Pos: p.Change.Pos,
-				Text: fmt.Sprintf(
-					"Adding a non-nullable %q column %q will fail in case table %q is not empty",
-					tt, p.Column.Name, p.Table.Name,
-				),
-			},
-		}, nil
+func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err error) {
+	tt, err := sqlite.FormatType(p.Column.Type.Type)
+	if err != nil {
+		return nil, err
 	}
-	return datadepend.New(opts)
+	return []sqlcheck.Diagnostic{
+		{
+			Pos: p.Change.Pos,
+			Text: fmt.Sprintf(
+				"Adding a non-nullable %q column %q will fail in case table %q is not empty",
+				tt, p.Column.Name, p.Table.Name,
+			),
+		},
+	}, nil
 }
 
 func init() {
 	sqlcheck.Register(sqlite.DriverName, func(r *schemahcl.Resource) (sqlcheck.Analyzer, error) {
-		dest, err := destructive.New(r)
+		ds, err := destructive.New(r)
+		if err != nil {
+			return nil, err
+		}
+		dd, err := datadepend.New(r, datadepend.Handler{
+			AddNotNull: addNotNull,
+		})
 		if err != nil {
 			return nil, err
 		}
@@ -80,8 +81,7 @@ func init() {
 				p.File.Changes = changes
 				return nil
 			}),
-			dest,
-			NewDataDepend(nil),
+			ds, dd,
 		}, nil
 	})
 }


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index b3e7c1ee..610118af 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -487,7 +487,7 @@ func TestMigrate_Lint(t *testing.T) {
 		"--latest", "1",
 	)
 	require.Error(t, err)
-	require.Equal(t, "Destructive changes detected in file 2.sql:\n\n\tL1: Dropping table \"t\"\n\n", s)
+	require.Equal(t, "2.sql: destructive change detected:\n\n\tL1: Dropping table \"t\"\n\n", s)
 	s, err = runCmd(
 		Root, "migrate", "lint",
 		"--dir", "file://"+p,
diff --git a/cmd/atlas/internal/lint/run_test.go b/cmd/atlas/internal/lint/run_test.go
index aece1e0b..f1de95b4 100644
--- a/cmd/atlas/internal/lint/run_test.go
+++ b/cmd/atlas/internal/lint/run_test.go
@@ -50,7 +50,7 @@ func TestRunner_Run(t *testing.T) {
 	require.Len(t, changes, 2)
 	require.Equal(t, "CREATE TABLE pets (id INT)", changes[0].Stmt)
 	require.Equal(t, "DROP TABLE users", changes[1].Stmt)
-	require.Equal(t, `Report 1. File "2.sql":
+	require.Equal(t, `2.sql: Report 1:
 
 	L1: Diagnostic 1
 
@@ -91,10 +91,10 @@ sqlite3, migrations
 Steps:
 {"Name":"Detect New Migration Files","Text":"Found 1 new migration files (from 2 total)"}
 {"Name":"Replay Migration Files","Text":"Loaded 1 changes on dev database"}
-{"Name":"Analyze 2.sql","Text":"1 reports were found in analysis","Result":{"Name":"2.sql","Text":"CREATE TABLE pets (id INT)\nDROP TABLE users","Reports":[{"Text":"Report 2. File \"2.sql\"","Diagnostics":[{"Pos":1,"Text":"Diagnostic 1"},{"Pos":2,"Text":"Diagnostic 2"}]}]}}
+{"Name":"Analyze 2.sql","Text":"1 reports were found in analysis","Result":{"Name":"2.sql","Text":"CREATE TABLE pets (id INT)\nDROP TABLE users","Reports":[{"Text":"Report 2","Diagnostics":[{"Pos":1,"Text":"Diagnostic 1"},{"Pos":2,"Text":"Diagnostic 2"}]}]}}
 
 Files:
-{"Name":"2.sql","Text":"CREATE TABLE pets (id INT)\nDROP TABLE users","Reports":[{"Text":"Report 2. File \"2.sql\"","Diagnostics":[{"Pos":1,"Text":"Diagnostic 1"},{"Pos":2,"Text":"Diagnostic 2"}]}]}
+{"Name":"2.sql","Text":"CREATE TABLE pets (id INT)\nDROP TABLE users","Reports":[{"Text":"Report 2","Diagnostics":[{"Pos":1,"Text":"Diagnostic 1"},{"Pos":2,"Text":"Diagnostic 2"}]}]}
 
 
 Current Schema:
@@ -129,7 +129,7 @@ type testAnalyzer struct {
 func (t *testAnalyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 	t.passes = append(t.passes, p)
 	r := sqlcheck.Report{
-		Text: fmt.Sprintf("Report %d. File %q", len(t.passes), p.File.Name()),
+		Text: fmt.Sprintf("Report %d", len(t.passes)),
 	}
 	for i := 1; i <= len(t.passes); i++ {
 		r.Diagnostics = append(r.Diagnostics, sqlcheck.Diagnostic{
diff --git a/internal/integration/testdata/mysql/cli-migrate-lint-destructive.txt b/internal/integration/testdata/mysql/cli-migrate-lint-destructive.txt
index 26a3226a..6b2058cb 100644
--- a/internal/integration/testdata/mysql/cli-migrate-lint-destructive.txt
+++ b/internal/integration/testdata/mysql/cli-migrate-lint-destructive.txt
@@ -38,7 +38,7 @@ ALTER TABLE users RENAME TO user, RENAME COLUMN id TO oid;
 ALTER TABLE user ADD COLUMN id int, DROP COLUMN oid;
 
 -- expected3.txt --
-Destructive changes detected in file 3.sql:
+3.sql: destructive change detected:
 
 	L1: Dropping non-virtual column "oid"
 
diff --git a/internal/integration/testdata/sqlite/cli-apply-directory.txt b/internal/integration/testdata/sqlite/cli-apply-directory.txt
index 797504c8..46afbac8 100644
--- a/internal/integration/testdata/sqlite/cli-apply-directory.txt
+++ b/internal/integration/testdata/sqlite/cli-apply-directory.txt
@@ -4,6 +4,12 @@ stderr 'no schema files found'
 atlas schema apply -f schema/ -u URL --auto-approve
 cmpshow users expected.sql
 
+atlas schema apply -f schema/ -u URL --auto-approve --dry-run --exclude "*.*.id" > inspected.txt
+cmp inspected.txt alter.txt
+
+atlas schema apply -f schema/ -u URL --auto-approve --dry-run --exclude "*.users" > inspected.txt
+cmp inspected.txt create.txt
+
 -- schema/users.hcl --
 table "users" {
   schema = schema.main
@@ -23,4 +29,12 @@ schema "main" {
 -- empty/hello.txt --
 hello, world!
 -- expected.sql --
-CREATE TABLE `users` (`id` int NOT NULL, `status` text NULL DEFAULT 'hello')
\ No newline at end of file
+CREATE TABLE `users` (`id` int NOT NULL, `status` text NULL DEFAULT 'hello')
+-- alter.txt --
+-- Planned Changes:
+-- Add column "id" to table: "users"
+ALTER TABLE `users` ADD COLUMN `id` int NOT NULL
+-- create.txt --
+-- Planned Changes:
+-- Create "users" table
+CREATE TABLE `users` (`id` int NOT NULL, `status` text NULL DEFAULT 'hello')
diff --git a/internal/integration/testdata/sqlite/cli-migrate-lint-add-notnull.txt b/internal/integration/testdata/sqlite/cli-migrate-lint-add-notnull.txt
index e3ad8681..6a220c45 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-lint-add-notnull.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-lint-add-notnull.txt
@@ -13,7 +13,7 @@ ALTER TABLE users ADD COLUMN c2 int NOT NULL;
 ALTER TABLE users ADD COLUMN c3 int NOT NULL DEFAULT 1;
 
 -- expected1.txt --
-Data dependent changes detected in file 2.sql:
+2.sql: data dependent change detected:
 
 	L1: Adding a non-nullable "int" column "c2" will fail in case table "users" is not empty
 
diff --git a/internal/integration/testdata/sqlite/cli-migrate-lint-destructive.txt b/internal/integration/testdata/sqlite/cli-migrate-lint-destructive.txt
index bcc59a29..7bd9fe2c 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-lint-destructive.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-lint-destructive.txt
@@ -20,16 +20,16 @@ DROP TABLE users;
 DROP TABLE pets;
 
 -- expected1.txt --
-Destructive changes detected in file 3.sql:
+3.sql: destructive change detected:
 
 	L1: Dropping table "pets"
 
 -- expected2.txt --
-Destructive changes detected in file 2.sql:
+2.sql: destructive change detected:
 
 	L1: Dropping table "users"
 
-Destructive changes detected in file 3.sql:
+3.sql: destructive change detected:
 
 	L1: Dropping table "pets"
 
diff --git a/internal/integration/testdata/sqlite/cli-migrate-lint-minimal-env.txt b/internal/integration/testdata/sqlite/cli-migrate-lint-minimal-env.txt
index 02d59c3f..86be9afc 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-lint-minimal-env.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-lint-minimal-env.txt
@@ -11,7 +11,7 @@ CREATE TABLE users (id int);
 -- migrations/2.sql --
 DROP TABLE users;
 -- expected1.txt --
-Destructive changes detected in file 2.sql:
+2.sql: destructive change detected:
 
 	L1: Dropping table "users"
 
diff --git a/sql/mysql/mysqlcheck/mysqlcheck_test.go b/sql/mysql/mysqlcheck/mysqlcheck_test.go
index 3fcac4d7..f606fea1 100644
--- a/sql/mysql/mysqlcheck/mysqlcheck_test.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck_test.go
@@ -10,7 +10,7 @@ import (
 
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/mysql"
-	"ariga.io/atlas/sql/mysql/mysqlcheck"
+	_ "ariga.io/atlas/sql/mysql/mysqlcheck"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
 	"ariga.io/atlas/sql/sqlclient"
@@ -60,7 +60,8 @@ func TestDataDepend_MySQL_ImplicitUpdate(t *testing.T) {
 			}),
 		}
 	)
-	az := mysqlcheck.NewDataDepend(nil)
+	az, err := sqlcheck.AnalyzerFor(mysql.DriverName, 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`)
@@ -105,7 +106,8 @@ func TestDataDepend_MySQL8_ImplicitUpdate(t *testing.T) {
 			}),
 		}
 	)
-	az := mysqlcheck.NewDataDepend(nil)
+	az, err := sqlcheck.AnalyzerFor(mysql.DriverName, nil)
+	require.NoError(t, err)
 	require.NoError(t, az.Analyze(context.Background(), pass))
 	require.Equal(t,
 		report.Diagnostics[0].Text,
@@ -152,7 +154,8 @@ func TestDataDepend_MySQL_MightFail(t *testing.T) {
 			}),
 		}
 	)
-	az := mysqlcheck.NewDataDepend(nil)
+	az, err := sqlcheck.AnalyzerFor(mysql.DriverName, 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`)
@@ -207,7 +210,8 @@ func TestDataDepend_Maria_ImplicitUpdate(t *testing.T) {
 			}),
 		}
 	)
-	az := mysqlcheck.NewDataDepend(nil)
+	az, err := sqlcheck.AnalyzerFor(mysql.DriverName, 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 ""`)
diff --git a/sql/postgres/postgrescheck/postgrescheck_test.go b/sql/postgres/postgrescheck/postgrescheck_test.go
new file mode 100644
index 00000000..5c1541d6
--- /dev/null
+++ b/sql/postgres/postgrescheck/postgrescheck_test.go
@@ -0,0 +1,63 @@
+// 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 postgrescheck_test
+
+import (
+	"context"
+	"testing"
+
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/postgres"
+	_ "ariga.io/atlas/sql/postgres/postgrescheck"
+	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlcheck"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDataDepend_MightFail(t *testing.T) {
+	var (
+		report *sqlcheck.Report
+		pass   = &sqlcheck.Pass{
+			File: &sqlcheck.File{
+				File: testFile{name: "1.sql"},
+				Changes: []*sqlcheck.Change{
+					{
+						Stmt: "ALTER TABLE users",
+						Changes: schema.Changes{
+							&schema.ModifyTable{
+								T: schema.NewTable("users").
+									SetSchema(schema.New("test")).
+									AddColumns(
+										schema.NewIntColumn("a", postgres.TypeInt),
+										schema.NewIntColumn("b", postgres.TypeInt),
+									),
+								Changes: []schema.Change{
+									&schema.AddColumn{C: schema.NewTimeColumn("b", postgres.TypeInt)},
+								},
+							},
+						},
+					},
+				},
+			},
+			Reporter: sqlcheck.ReportWriterFunc(func(r sqlcheck.Report) {
+				report = &r
+			}),
+		}
+	)
+	az, err := sqlcheck.AnalyzerFor(postgres.DriverName, 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" will fail in case table "users" is not empty`)
+}
+
+type testFile struct {
+	name string
+	migrate.File
+}
+
+func (t testFile) Name() string {
+	return t.name
+}
diff --git a/sql/sqlcheck/datadepend/datadepend_test.go b/sql/sqlcheck/datadepend/datadepend_test.go
index d2b99a44..e349a640 100644
--- a/sql/sqlcheck/datadepend/datadepend_test.go
+++ b/sql/sqlcheck/datadepend/datadepend_test.go
@@ -8,6 +8,9 @@ import (
 	"context"
 	"testing"
 
+	"ariga.io/atlas/schemahcl"
+	"ariga.io/atlas/sql/internal/specutil"
+
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
@@ -60,10 +63,11 @@ func TestAnalyzer_AddUniqueIndex(t *testing.T) {
 			}),
 		}
 	)
-	az := datadepend.New(datadepend.Options{})
-	err := az.Analyze(context.Background(), pass)
+	az, err := datadepend.New(nil, datadepend.Handler{})
+	require.NoError(t, err)
+	err = az.Analyze(context.Background(), pass)
 	require.NoError(t, err)
-	require.Equal(t, "Data dependent changes detected in file 1.sql", report.Text)
+	require.Equal(t, "data dependent change detected", report.Text)
 	require.Len(t, report.Diagnostics, 1)
 	require.Equal(t, `Adding a unique index "idx_b" on table "users" might fail in case column "b" contains duplicate entries`, report.Diagnostics[0].Text)
 }
@@ -116,18 +120,19 @@ func TestAnalyzer_ModifyUniqueIndex(t *testing.T) {
 			}),
 		}
 	)
-	az := datadepend.New(datadepend.Options{})
-	err := az.Analyze(context.Background(), pass)
+	az, err := datadepend.New(nil, datadepend.Handler{})
 	require.NoError(t, err)
-	require.Equal(t, "Data dependent changes detected in file 1.sql", report.Text)
+	err = az.Analyze(context.Background(), pass)
+	require.NoError(t, err)
+	require.Equal(t, "data dependent change detected", report.Text)
 	require.Len(t, report.Diagnostics, 1)
 	require.Equal(t, `Modifying an index "idx_b" on table "users" might fail in case of duplicate entries`, report.Diagnostics[0].Text)
 }
 
 func TestAnalyzer_Options(t *testing.T) {
 	var (
-		off  bool
-		pass = &sqlcheck.Pass{
+		report *sqlcheck.Report
+		pass   = &sqlcheck.Pass{
 			Dev: &sqlclient.Client{},
 			File: &sqlcheck.File{
 				File: testFile{name: "1.sql"},
@@ -161,13 +166,24 @@ func TestAnalyzer_Options(t *testing.T) {
 				},
 			},
 			Reporter: sqlcheck.ReportWriterFunc(func(r sqlcheck.Report) {
-				t.Fatal("unexpected report", r)
+				report = &r
 			}),
 		}
 	)
-	az := datadepend.New(datadepend.Options{UniqueIndex: &off})
-	err := az.Analyze(context.Background(), pass)
+	az, err := datadepend.New(&schemahcl.Resource{
+		Children: []*schemahcl.Resource{
+			{
+				Type: "data_depend",
+				Attrs: []*schemahcl.Attr{
+					specutil.BoolAttr("error", true),
+				},
+			},
+		},
+	}, datadepend.Handler{})
 	require.NoError(t, err)
+	err = az.Analyze(context.Background(), pass)
+	require.EqualError(t, err, "data dependent change detected")
+	require.NotNil(t, report)
 }
 
 type testFile struct {
diff --git a/sql/sqlcheck/destructive/destructive_test.go b/sql/sqlcheck/destructive/destructive_test.go
index 730eefa2..5270abe3 100644
--- a/sql/sqlcheck/destructive/destructive_test.go
+++ b/sql/sqlcheck/destructive/destructive_test.go
@@ -65,7 +65,7 @@ func TestAnalyzer_DropTable(t *testing.T) {
 	require.NoError(t, err)
 	err = az.Analyze(context.Background(), pass)
 	require.Error(t, err)
-	require.Equal(t, `Destructive changes detected in file 1.sql`, report.Text)
+	require.Equal(t, "2 destructive changes detected", report.Text)
 	require.Len(t, report.Diagnostics, 2)
 	require.Equal(t, `Dropping table "users"`, report.Diagnostics[0].Text)
 	require.Equal(t, `Dropping table "posts"`, report.Diagnostics[1].Text)
@@ -162,7 +162,7 @@ func TestAnalyzer_DropSchema(t *testing.T) {
 	require.NoError(t, err)
 	err = az.Analyze(context.Background(), pass)
 	require.NoError(t, err)
-	require.Equal(t, `Destructive changes detected in file 1.sql`, report.Text)
+	require.Equal(t, "2 destructive changes detected", report.Text)
 	require.Len(t, report.Diagnostics, 2)
 	require.Equal(t, `Dropping non-empty schema "test" with 2 tables`, report.Diagnostics[0].Text)
 	require.Equal(t, `Dropping schema "market"`, report.Diagnostics[1].Text)
@@ -226,6 +226,7 @@ func TestAnalyzer_DropColumn(t *testing.T) {
 	err = az.Analyze(context.Background(), pass)
 	require.NoError(t, err)
 	require.Len(t, report.Diagnostics, 1)
+	require.Equal(t, "destructive change detected", report.Text)
 	require.Equal(t, `Dropping non-virtual column "c"`, report.Diagnostics[0].Text)
 }
 
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck_test.go b/sql/sqlite/sqlitecheck/sqlitecheck_test.go
index f05734a9..da3e04ee 100644
--- a/sql/sqlite/sqlitecheck/sqlitecheck_test.go
+++ b/sql/sqlite/sqlitecheck/sqlitecheck_test.go
@@ -111,9 +111,9 @@ func TestDetectModifyTable(t *testing.T) {
 	az, err := sqlcheck.AnalyzerFor(sqlite.DriverName, nil)
 	require.NoError(t, err)
 	err = az.Analyze(context.Background(), pass)
-	require.EqualError(t, err, "Destructive changes detected in file 1.sql")
+	require.EqualError(t, err, "3 destructive changes detected")
 
-	require.Equal(t, report.Text, "Destructive changes detected in file 1.sql")
+	require.Equal(t, report.Text, "3 destructive changes detected")
 	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"`)