Repository: ariga/atlas
Pull request #2359 (2023-12-11 10:48:52)

sql: implement migrate.StmtScanner in drivers


Patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 80936ea4..c2d99c18 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -880,7 +880,7 @@ func migrateImportRun(cmd *cobra.Command, _ []string, flags migrateImportFlags)
 	// Extract the statements for each of the migration files,
 	// add them to a plan to format with the DefaultFormatter.
 	for _, f := range ff {
-		stmts, err := f.StmtDecls()
+		stmts, err := f.StmtDecls() // Not driver aware.
 		if err != nil {
 			return err
 		}
diff --git a/cmd/atlas/internal/migratelint/lint.go b/cmd/atlas/internal/migratelint/lint.go
index 48774c6e..6a134e17 100644
--- a/cmd/atlas/internal/migratelint/lint.go
+++ b/cmd/atlas/internal/migratelint/lint.go
@@ -270,7 +270,7 @@ func (d *DevLoader) base(ctx context.Context, base []migrate.File) (*schema.Real
 		base = base[i:]
 	}
 	for _, f := range base {
-		stmt, err := f.StmtDecls()
+		stmt, err := migrate.FileStmtDecls(d.Dev, f)
 		if err != nil {
 			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 		}
@@ -286,7 +286,7 @@ func (d *DevLoader) base(ctx context.Context, base []migrate.File) (*schema.Real
 // next returns the next state of the database after executing the statements in
 // the file. The changes detected by the statements are attached to the file.
 func (d *DevLoader) next(ctx context.Context, f *sqlcheck.File, start *schema.Realm) (current *schema.Realm, err error) {
-	stmts, err := f.StmtDecls()
+	stmts, err := migrate.FileStmtDecls(d.Dev, f)
 	if err != nil {
 		return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 	}
diff --git a/cmd/atlas/internal/sqlparse/myparse/myparse.go b/cmd/atlas/internal/sqlparse/myparse/myparse.go
index af7ed9ae..fa7de71c 100644
--- a/cmd/atlas/internal/sqlparse/myparse/myparse.go
+++ b/cmd/atlas/internal/sqlparse/myparse/myparse.go
@@ -24,9 +24,9 @@ import (
 type Parser struct{}
 
 // ColumnFilledBefore checks if the column was filled before the given position.
-func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.Column, pos int) (bool, error) {
+func (p *Parser) ColumnFilledBefore(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int) (bool, error) {
 	pr := parser.New()
-	return parseutil.MatchStmtBefore(f, pos, func(s *migrate.Stmt) (bool, error) {
+	return parseutil.MatchStmtBefore(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := pr.ParseOneStmt(s.Text, "", "")
 		if err != nil {
 			return false, err
@@ -58,11 +58,7 @@ func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.C
 }
 
 // ColumnFilledAfter checks if the column that matches the given value was filled after the position.
-func (p *Parser) ColumnFilledAfter(f migrate.File, t *schema.Table, c *schema.Column, pos int, match any) (bool, error) {
-	stmts, err := f.StmtDecls()
-	if err != nil {
-		return false, err
-	}
+func (p *Parser) ColumnFilledAfter(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int, match any) (bool, error) {
 	switch i := slices.IndexFunc(stmts, func(s *migrate.Stmt) bool {
 		return s.Pos >= pos
 	}); i {
@@ -156,9 +152,9 @@ func (p *Parser) ColumnHasReferences(stmt *migrate.Stmt, c1 *schema.Column) (boo
 }
 
 // CreateViewAfter checks if a view was created after the position with the given name to a table.
-func (p *Parser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error) {
+func (p *Parser) CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error) {
 	pr := parser.New()
-	return parseutil.MatchStmtAfter(f, pos, func(s *migrate.Stmt) (bool, error) {
+	return parseutil.MatchStmtAfter(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := pr.ParseOneStmt(s.Text, "", "")
 		if err != nil {
 			return false, err
diff --git a/cmd/atlas/internal/sqlparse/parseutil/parseutil.go b/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
index bf7995b9..59eb3cd3 100644
--- a/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
+++ b/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
@@ -77,11 +77,7 @@ func RenameTable(changes schema.Changes, r *Rename) schema.Changes {
 }
 
 // MatchStmtBefore reports if the file contains any statement that matches the predicate before the given position.
-func MatchStmtBefore(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
-	stmts, err := f.StmtDecls()
-	if err != nil {
-		return false, err
-	}
+func MatchStmtBefore(stmts []*migrate.Stmt, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
 	i := slices.IndexFunc(stmts, func(s *migrate.Stmt) bool {
 		return s.Pos >= pos
 	})
@@ -101,11 +97,7 @@ func MatchStmtBefore(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error
 }
 
 // MatchStmtAfter reports if the file contains any statement that matches the predicate after the given position.
-func MatchStmtAfter(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
-	stmts, err := f.StmtDecls()
-	if err != nil {
-		return false, err
-	}
+func MatchStmtAfter(stmts []*migrate.Stmt, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
 	i := slices.IndexFunc(stmts, func(s *migrate.Stmt) bool {
 		return s.Pos > pos
 	})
@@ -124,17 +116,3 @@ func MatchStmtAfter(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)
 	}
 	return false, nil
 }
-
-func max(i, j int) int {
-	if i > j {
-		return i
-	}
-	return j
-}
-
-func min(i, j int) int {
-	if i < j {
-		return i
-	}
-	return j
-}
diff --git a/cmd/atlas/internal/sqlparse/pgparse/pgparse.go b/cmd/atlas/internal/sqlparse/pgparse/pgparse.go
index 7a5efe14..97c25106 100644
--- a/cmd/atlas/internal/sqlparse/pgparse/pgparse.go
+++ b/cmd/atlas/internal/sqlparse/pgparse/pgparse.go
@@ -22,8 +22,8 @@ import (
 type Parser struct{}
 
 // ColumnFilledBefore checks if the column was filled before the given position.
-func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.Column, pos int) (bool, error) {
-	return parseutil.MatchStmtBefore(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *Parser) ColumnFilledBefore(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int) (bool, error) {
+	return parseutil.MatchStmtBefore(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		tr, err := pgquery.Parse(s.Text)
 		if err != nil {
 			return false, err
@@ -61,8 +61,8 @@ func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.C
 }
 
 // CreateViewAfter checks if a view was created after the position with the given name to a table.
-func (p *Parser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error) {
-	return parseutil.MatchStmtAfter(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *Parser) CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error) {
+	return parseutil.MatchStmtAfter(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		tr, err := pgquery.Parse(s.Text)
 		if err != nil {
 			return false, err
@@ -78,7 +78,7 @@ func (p *Parser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool
 			return false, nil
 		}
 		from := v.Query.GetSelectStmt().GetFromClause()
-		if f == nil || len(from) != 1 {
+		if len(from) != 1 {
 			return false, nil
 		}
 		return from[0].GetRangeVar().GetRelname() == new, nil
diff --git a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go
index d7f66730..517f12b9 100644
--- a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go
+++ b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go
@@ -145,8 +145,8 @@ func (s *Stmt) CreateView(name string) (*Create_view_stmtContext, bool) {
 type FileParser struct{}
 
 // ColumnFilledBefore checks if the column was filled before the given position.
-func (p *FileParser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.Column, pos int) (bool, error) {
-	return parseutil.MatchStmtBefore(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *FileParser) ColumnFilledBefore(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int) (bool, error) {
+	return parseutil.MatchStmtBefore(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := ParseStmt(s.Text)
 		if err != nil {
 			return false, err
@@ -189,8 +189,8 @@ func (p *FileParser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *sche
 }
 
 // CreateViewAfter checks if a view was created after the position with the given name to a table.
-func (p *FileParser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error) {
-	return parseutil.MatchStmtAfter(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *FileParser) CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error) {
+	return parseutil.MatchStmtAfter(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := ParseStmt(s.Text)
 		if err != nil {
 			return false, err
diff --git a/cmd/atlas/internal/sqlparse/sqlparse.go b/cmd/atlas/internal/sqlparse/sqlparse.go
index dd618746..710a9e30 100644
--- a/cmd/atlas/internal/sqlparse/sqlparse.go
+++ b/cmd/atlas/internal/sqlparse/sqlparse.go
@@ -28,7 +28,15 @@ type Parser interface {
 	//	UPDATE <table> SET <column> = <value>
 	//	UPDATE <table> SET <column> = <value> WHERE <column> IS NULL
 	//
-	ColumnFilledBefore(migrate.File, *schema.Table, *schema.Column, int) (bool, error)
+	ColumnFilledBefore([]*migrate.Stmt, *schema.Table, *schema.Column, int) (bool, error)
+
+	// CreateViewAfter checks if a view was created after the position with the given name
+	// to a table. For example:
+	//
+	//	ALTER TABLE `users` RENAME TO `Users`
+	//	CREATE VIEW `users` AS SELECT * FROM `Users`
+	//
+	CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error)
 }
 
 // drivers specific fixers.
diff --git a/sql/migrate/lex.go b/sql/migrate/lex.go
index 7babf261..89d1fbf4 100644
--- a/sql/migrate/lex.go
+++ b/sql/migrate/lex.go
@@ -44,7 +44,7 @@ func (s *Stmt) Directive(name string) (ds []string) {
 
 // Stmts provides a generic implementation for extracting SQL statements from the given file contents.
 func Stmts(input string) ([]*Stmt, error) {
-	return (&StmtScanner{
+	return (&Scanner{
 		ScannerOptions: ScannerOptions{
 			// Default options for backward compatibility.
 			MatchBegin:       false,
@@ -54,9 +54,40 @@ func Stmts(input string) ([]*Stmt, error) {
 	}).Scan(input)
 }
 
+// FileStmtDecls scans atlas-format file statements using
+// the Driver implementation, if implemented.
+func FileStmtDecls(drv Driver, f File) ([]*Stmt, error) {
+	s, ok1 := drv.(StmtScanner)
+	_, ok2 := f.(*LocalFile)
+	if !ok1 || !ok2 {
+		return f.StmtDecls()
+	}
+	return s.ScanStmts(string(f.Bytes()))
+}
+
+// FileStmts is like FileStmtDecls but returns only the
+// statement text without the extra info.
+func FileStmts(drv Driver, f File) ([]string, error) {
+	s, err := FileStmtDecls(drv, f)
+	if err != nil {
+		return nil, err
+	}
+	stmts := make([]string, len(s))
+	for i := range s {
+		stmts[i] = s[i].Text
+	}
+	return stmts, nil
+}
+
 type (
-	// StmtScanner scanning SQL statements from migration and schema files.
-	StmtScanner struct {
+	// StmtScanner interface for scanning SQL statements from migration
+	// and schema files and can be optionally implemented by drivers.
+	StmtScanner interface {
+		ScanStmts(input string) ([]*Stmt, error)
+	}
+
+	// Scanner scanning SQL statements from migration and schema files.
+	Scanner struct {
 		ScannerOptions
 		// scanner state.
 		src, input string   // src and current input text
@@ -66,6 +97,7 @@ type (
 		delim      string   // configured delimiter
 		comments   []string // collected comments
 	}
+
 	// ScannerOptions controls the behavior of the scanner.
 	ScannerOptions struct {
 		// MatchBegin enables matching for BEGIN ... END statements block.
@@ -78,7 +110,7 @@ type (
 )
 
 // Scan scans the statement in the given input.
-func (s *StmtScanner) Scan(input string) ([]*Stmt, error) {
+func (s *Scanner) Scan(input string) ([]*Stmt, error) {
 	var stmts []*Stmt
 	if err := s.init(input); err != nil {
 		return nil, err
@@ -96,7 +128,7 @@ func (s *StmtScanner) Scan(input string) ([]*Stmt, error) {
 }
 
 // init initializes the scanner state.
-func (s *StmtScanner) init(input string) error {
+func (s *Scanner) init(input string) error {
 	s.comments = nil
 	s.pos, s.total, s.width = 0, 0, 0
 	s.src, s.input, s.delim = input, input, delimiter
@@ -128,7 +160,7 @@ var (
 	reEnd         = regexp.MustCompile(`(?i)^\s*END\s*`)
 )
 
-func (s *StmtScanner) stmt() (*Stmt, error) {
+func (s *Scanner) stmt() (*Stmt, error) {
 	var (
 		depth, openingPos int
 		text              string
@@ -202,7 +234,7 @@ Scan:
 	return s.emit(text), nil
 }
 
-func (s *StmtScanner) next() rune {
+func (s *Scanner) next() rune {
 	if s.pos >= len(s.input) {
 		return eos
 	}
@@ -212,19 +244,19 @@ func (s *StmtScanner) next() rune {
 	return r
 }
 
-func (s *StmtScanner) pick() rune {
+func (s *Scanner) pick() rune {
 	p, w := s.pos, s.width
 	r := s.next()
 	s.pos, s.width = p, w
 	return r
 }
 
-func (s *StmtScanner) addPos(p int) {
+func (s *Scanner) addPos(p int) {
 	s.pos += p
 	s.total += p
 }
 
-func (s *StmtScanner) skipQuote(quote rune) error {
+func (s *Scanner) skipQuote(quote rune) error {
 	pos := s.pos
 	for {
 		switch r := s.next(); {
@@ -238,7 +270,7 @@ func (s *StmtScanner) skipQuote(quote rune) error {
 	}
 }
 
-func (s *StmtScanner) skipDollarQuote() error {
+func (s *Scanner) skipDollarQuote() error {
 	m := reDollarQuote.FindString(s.input[s.pos-1:])
 	if m == "" {
 		return s.error(s.pos, "unexpected dollar quote")
@@ -259,13 +291,13 @@ func (s *StmtScanner) skipDollarQuote() error {
 	}
 }
 
-func (s *StmtScanner) skipBeginAtomic() error {
+func (s *Scanner) skipBeginAtomic() error {
 	m := reBeginAtomic.FindString(s.input[s.pos-1:])
 	if m == "" {
 		return s.error(s.pos, "unexpected missing BEGIN ATOMIC block")
 	}
 	s.addPos(len(m) - 1)
-	body := &StmtScanner{ScannerOptions: s.ScannerOptions}
+	body := &Scanner{ScannerOptions: s.ScannerOptions}
 	if err := body.init(s.input[s.pos:]); err != nil {
 		return err
 	}
@@ -285,13 +317,13 @@ func (s *StmtScanner) skipBeginAtomic() error {
 	return nil
 }
 
-func (s *StmtScanner) skipBegin() error {
+func (s *Scanner) skipBegin() error {
 	m := reBegin.FindString(s.input[s.pos-1:])
 	if m == "" {
 		return s.error(s.pos, "unexpected missing BEGIN block")
 	}
 	s.addPos(len(m) - 1)
-	group := &StmtScanner{ScannerOptions: s.ScannerOptions}
+	group := &Scanner{ScannerOptions: s.ScannerOptions}
 	if err := group.init(s.input[s.pos:]); err != nil {
 		return err
 	}
@@ -311,7 +343,7 @@ func (s *StmtScanner) skipBegin() error {
 	return nil
 }
 
-func (s *StmtScanner) comment(left, right string) {
+func (s *Scanner) comment(left, right string) {
 	i := strings.Index(s.input[s.pos:], right)
 	// Not a comment.
 	if i == -1 {
@@ -335,13 +367,13 @@ func (s *StmtScanner) comment(left, right string) {
 	s.skipSpaces()
 }
 
-func (s *StmtScanner) skipSpaces() {
+func (s *Scanner) skipSpaces() {
 	n := len(s.input)
 	s.input = strings.TrimLeftFunc(s.input, unicode.IsSpace)
 	s.total += n - len(s.input)
 }
 
-func (s *StmtScanner) emit(text string) *Stmt {
+func (s *Scanner) emit(text string) *Stmt {
 	stmt := &Stmt{Pos: s.total - len(text), Text: text, Comments: s.comments}
 	s.input = s.input[s.pos:]
 	s.pos = 0
@@ -356,7 +388,7 @@ func (s *StmtScanner) emit(text string) *Stmt {
 
 // delimCmd checks if the scanned "DELIMITER"
 // text represents an actual delimiter command.
-func (s *StmtScanner) delimCmd() error {
+func (s *Scanner) delimCmd() error {
 	// A space must come after the delimiter.
 	if s.pick() != ' ' {
 		return nil
@@ -377,7 +409,7 @@ func (s *StmtScanner) delimCmd() error {
 	return nil
 }
 
-func (s *StmtScanner) setDelim(d string) error {
+func (s *Scanner) setDelim(d string) error {
 	if d == "" {
 		return errors.New("empty delimiter")
 	}
@@ -386,7 +418,7 @@ func (s *StmtScanner) setDelim(d string) error {
 	return nil
 }
 
-func (s *StmtScanner) error(pos int, format string, args ...any) error {
+func (s *Scanner) error(pos int, format string, args ...any) error {
 	format = "%d:%d: " + format
 	var (
 		p    = len(s.src) - len(s.input) + pos
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 5223ba57..ee380b5f 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -780,7 +780,7 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning checksum from %q: %w", m.Name(), err)
 	}
-	stmts, err := m.Stmts()
+	stmts, err := FileStmts(e.drv, m)
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning statements from %q: %w", m.Name(), err)
 	}
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 2547cff6..0035c2c7 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -44,6 +44,7 @@ type (
 
 var _ interface {
 	migrate.Snapshoter
+	migrate.StmtScanner
 	migrate.CleanChecker
 	schema.TypeParseFormatter
 } = (*Driver)(nil)
@@ -254,6 +255,18 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// ScanStmts implements migrate.StmtScanner.
+func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
+	return (&migrate.Scanner{
+		ScannerOptions: migrate.ScannerOptions{
+			MatchBegin: true,
+			// The following are not support by MySQL/MariaDB.
+			MatchBeginAtomic: false,
+			MatchDollarQuote: false,
+		},
+	}).Scan(input)
+}
+
 func acquire(ctx context.Context, conn schema.ExecQuerier, name string, timeout time.Duration) error {
 	rows, err := conn.QueryContext(ctx, "SELECT GET_LOCK(?, ?)", name, int(timeout.Seconds()))
 	if err != nil {
diff --git a/sql/mysql/mysqlcheck/mysqlcheck.go b/sql/mysql/mysqlcheck/mysqlcheck.go
index 8d8cf003..5b794279 100644
--- a/sql/mysql/mysqlcheck/mysqlcheck.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck.go
@@ -142,12 +142,16 @@ func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err erro
 // columnFilledAfter checks if the column with the given value was filled after the current change.
 func columnFilledAfter(pass *datadepend.ColumnPass, matchValue string) bool {
 	p, ok := pass.File.Parser.(interface {
-		ColumnFilledAfter(migrate.File, *schema.Table, *schema.Column, int, any) (bool, error)
+		ColumnFilledAfter([]*migrate.Stmt, *schema.Table, *schema.Column, int, any) (bool, error)
 	})
 	if !ok {
 		return false
 	}
-	filled, _ := p.ColumnFilledAfter(pass.File, pass.Table, pass.Column, pass.Change.Stmt.Pos, matchValue)
+	stmts, err := migrate.FileStmtDecls(pass.Dev, pass.File)
+	if err != nil {
+		return false
+	}
+	filled, _ := p.ColumnFilledAfter(stmts, pass.Table, pass.Column, pass.Change.Stmt.Pos, matchValue)
 	return filled
 }
 
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index 4c5c9a79..9565d564 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -43,6 +43,7 @@ type (
 
 var _ interface {
 	migrate.Snapshoter
+	migrate.StmtScanner
 	migrate.CleanChecker
 	schema.TypeParseFormatter
 } = (*Driver)(nil)
@@ -315,6 +316,17 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// ScanStmts implements migrate.StmtScanner.
+func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
+	return (&migrate.Scanner{
+		ScannerOptions: migrate.ScannerOptions{
+			MatchBegin:       true,
+			MatchBeginAtomic: true,
+			MatchDollarQuote: true,
+		},
+	}).Scan(input)
+}
+
 func acquire(ctx context.Context, conn schema.ExecQuerier, id uint32, timeout time.Duration) error {
 	switch {
 	// With timeout (context-based).
diff --git a/sql/sqlcheck/datadepend/datadepend.go b/sql/sqlcheck/datadepend/datadepend.go
index 39e7fb90..417fbb79 100644
--- a/sql/sqlcheck/datadepend/datadepend.go
+++ b/sql/sqlcheck/datadepend/datadepend.go
@@ -152,7 +152,7 @@ func (a *Analyzer) Diagnostics(_ context.Context, p *sqlcheck.Pass) (diags []sql
 						diags = append(diags, d...)
 					// In case the altered column was not added in this file, and the column
 					// was changed nullable to non-nullable without back filling it with values.
-					case !ColumnFilled(p.File, m.T, c.From, sc.Stmt.Pos):
+					case !ColumnFilled(p, m.T, c.From, sc.Stmt.Pos):
 						diags = append(diags, sqlcheck.Diagnostic{
 							Code: codeModNotNullC,
 							Pos:  sc.Stmt.Pos,
@@ -181,15 +181,19 @@ func (a *Analyzer) Report(p *sqlcheck.Pass, diags []sqlcheck.Diagnostic) error {
 }
 
 // ColumnFilled checks if the column was filled with values before the given position.
-func ColumnFilled(f *sqlcheck.File, t *schema.Table, c *schema.Column, pos int) bool {
+func ColumnFilled(p *sqlcheck.Pass, t *schema.Table, c *schema.Column, pos int) bool {
 	// The parser used for parsing this file can check if the
 	// given nullable column was filled before the given position.
-	p, ok := f.Parser.(interface {
-		ColumnFilledBefore(migrate.File, *schema.Table, *schema.Column, int) (bool, error)
+	pr, ok := p.File.Parser.(interface {
+		ColumnFilledBefore([]*migrate.Stmt, *schema.Table, *schema.Column, int) (bool, error)
 	})
 	if !ok {
 		return false
 	}
-	filled, _ := p.ColumnFilledBefore(f, t, c, pos)
+	stmts, err := migrate.FileStmtDecls(p.Dev, p.File)
+	if err != nil {
+		return false
+	}
+	filled, _ := pr.ColumnFilledBefore(stmts, t, c, pos)
 	return filled
 }
diff --git a/sql/sqlcheck/incompatible/incompatible.go b/sql/sqlcheck/incompatible/incompatible.go
index a85b9f8c..52c604c3 100644
--- a/sql/sqlcheck/incompatible/incompatible.go
+++ b/sql/sqlcheck/incompatible/incompatible.go
@@ -51,7 +51,7 @@ func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 			switch c := c.(type) {
 			case *schema.RenameTable:
 				if p.File.SchemaSpan(c.From.Schema)&sqlcheck.SpanAdded == 0 && p.File.TableSpan(c.From)&sqlcheck.SpanAdded == 0 &&
-					!ViewForRenamedT(p.File, c.From.Name, c.To.Name, sc.Stmt.Pos) {
+					!ViewForRenamedT(p, c.From.Name, c.To.Name, sc.Stmt.Pos) {
 					diags = append(diags, sqlcheck.Diagnostic{
 						Code: codeRenameT,
 						Pos:  sc.Stmt.Pos,
@@ -83,16 +83,20 @@ func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 }
 
 // ViewForRenamedT checks if a view was created was a table that was renamed after the given position.
-func ViewForRenamedT(f *sqlcheck.File, old, new string, pos int) bool {
+func ViewForRenamedT(p *sqlcheck.Pass, old, new string, pos int) bool {
 	// The parser used for parsing this file can check if the
 	// given nullable column was filled before the given position.
-	p, ok := f.Parser.(interface {
-		CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error)
+	pr, ok := p.File.Parser.(interface {
+		CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error)
 	})
 	if !ok {
 		return false
 	}
-	created, _ := p.CreateViewAfter(f, old, new, pos)
+	stmts, err := migrate.FileStmtDecls(p.Dev, p.File)
+	if err != nil {
+		return false
+	}
+	created, _ := pr.CreateViewAfter(stmts, old, new, pos)
 	return created
 }
 
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index d1c56722..5c77cff5 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -43,6 +43,7 @@ type (
 
 var _ interface {
 	migrate.Snapshoter
+	migrate.StmtScanner
 	migrate.CleanChecker
 	schema.TypeParseFormatter
 } = (*Driver)(nil)
@@ -188,6 +189,18 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// ScanStmts implements migrate.StmtScanner.
+func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
+	return (&migrate.Scanner{
+		ScannerOptions: migrate.ScannerOptions{
+			MatchBegin: true,
+			// The following are not support by SQLite.
+			MatchBeginAtomic: false,
+			MatchDollarQuote: false,
+		},
+	}).Scan(input)
+}
+
 func acquireLock(path string, timeout time.Duration) (schema.UnlockFunc, error) {
 	lock, err := os.Create(path)
 	if err != nil {
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck.go b/sql/sqlite/sqlitecheck/sqlitecheck.go
index 5567a332..3c2389d9 100644
--- a/sql/sqlite/sqlitecheck/sqlitecheck.go
+++ b/sql/sqlite/sqlitecheck/sqlitecheck.go
@@ -41,7 +41,7 @@ func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err erro
 }
 
 func modifyNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err error) {
-	if p.Column.Default != nil || datadepend.ColumnFilled(p.File, p.Table, p.Column, p.Change.Stmt.Pos) {
+	if p.Column.Default != nil || datadepend.ColumnFilled(p.Pass, p.Table, p.Column, p.Change.Stmt.Pos) {
 		return nil, nil
 	}
 	return []sqlcheck.Diagnostic{


Test patch:
diff --git a/cmd/atlas/internal/sqlparse/myparse/myparse_test.go b/cmd/atlas/internal/sqlparse/myparse/myparse_test.go
index 486c14d4..bea8ba31 100644
--- a/cmd/atlas/internal/sqlparse/myparse/myparse_test.go
+++ b/cmd/atlas/internal/sqlparse/myparse/myparse_test.go
@@ -246,11 +246,10 @@ ALTER TABLE t MODIFY COLUMN c INT NOT NULL;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p myparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledBefore(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
+			var p myparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledBefore(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -321,11 +320,10 @@ UPDATE t SET c = CONCAT('tenant_', d) WHERE c = 0;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p myparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledAfter(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos, tt.matchValue)
+			var p myparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledAfter(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos, tt.matchValue)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -377,11 +375,10 @@ CREATE VIEW old AS SELECT a, b, c FROM new;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p myparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			created, err := p.CreateViewAfter(f, "old", "new", tt.pos)
+			var p myparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			created, err := p.CreateViewAfter(stmts, "old", "new", tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, created, tt.wantCreated)
 		})
diff --git a/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go b/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go
index 539c27a4..cf892244 100644
--- a/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go
+++ b/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go
@@ -281,11 +281,10 @@ func TestColumnFilledBefore(t *testing.T) {
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p pgparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledBefore(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
+			var p pgparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledBefore(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -345,11 +344,10 @@ CREATE VIEW old AS SELECT * FROM new;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p pgparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			created, err := p.CreateViewAfter(f, "old", "new", tt.pos)
+			var p pgparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			created, err := p.CreateViewAfter(stmts, "old", "new", tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, tt.wantCreated, created)
 		})
diff --git a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go
index c077dbfa..e62d0362 100644
--- a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go
+++ b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go
@@ -133,11 +133,10 @@ UPDATE t1 SET c = 2 WHERE c IS NULL;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p sqliteparse.FileParser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledBefore(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
+			var p sqliteparse.FileParser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledBefore(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -189,11 +188,10 @@ CREATE VIEW old AS SELECT a, b, c FROM new;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p sqliteparse.FileParser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			created, err := p.CreateViewAfter(f, "old", "new", tt.pos)
+			var p sqliteparse.FileParser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			created, err := p.CreateViewAfter(stmts, "old", "new", tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, created, tt.wantCreated)
 		})
diff --git a/sql/migrate/lex_test.go b/sql/migrate/lex_test.go
index 25e4c1d8..79e8ee43 100644
--- a/sql/migrate/lex_test.go
+++ b/sql/migrate/lex_test.go
@@ -29,7 +29,7 @@ func TestLocalFile_Stmts(t *testing.T) {
 }
 
 func TestScanner_StmtsGroup(t *testing.T) {
-	scan := &StmtScanner{}
+	scan := &Scanner{}
 	scan.MatchBegin = true
 	path := filepath.Join("testdata", "lexgroup")
 	dir, err := NewLocalDir(path)
diff --git a/sql/sqlcheck/incompatible/incompatible_test.go b/sql/sqlcheck/incompatible/incompatible_test.go
index c5d085a9..5f5168da 100644
--- a/sql/sqlcheck/incompatible/incompatible_test.go
+++ b/sql/sqlcheck/incompatible/incompatible_test.go
@@ -275,6 +275,6 @@ func (t testFile) StmtDecls() ([]*migrate.Stmt, error) {
 
 type testParser struct{ matchOn string }
 
-func (t testParser) CreateViewAfter(_ migrate.File, old, _ string, _ int) (bool, error) {
+func (t testParser) CreateViewAfter(_ []*migrate.Stmt, old, _ string, _ int) (bool, error) {
 	return t.matchOn == old, nil
 }