Repository: ariga/atlas
Pull request #1097 (2022-08-24 10:10:43)

sql/sqlcheck: move data-depend to the new options


Patch:
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/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-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"`)