Repository: ariga/atlas
Pull request #1113 (2022-08-31 10:51:21)

readme: include links to our social (twitter + discord)

gneyal: Changed the readme page. Links to bugs and new features will be addressed after new templates will be in place.
![Screen Shot 2022-08-31 at 14 00 09](https://user-images.githubusercontent.com/1525119/187664014-9043dcf1-f53d-4e69-b082-fc8316f18337.png)


Patch:
diff --git a/README.md b/README.md
index 2137757a..a585f3e8 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,30 @@
+<div align="center">
+     <img width="50%" align="center" style="display: block; "
+     src="https://blog.ariga.io/uploads/images/atlas_logo_text.png"/>
+</div>
+<h1 align="center">A Database Toolkit</h1>
+
+
+<div align="center">
+Manage your database schemas with Atlas CLI
+</div>
+
+<div align="center">
+     <p align="center">
+         <br />
+         <a href="https://atlasgo.io/getting-started/" rel="dofollow"><strong>Explore the docs »</strong></a>
+         <br /><br/>
+          <a href="https://discord.com/invite/zZ6sWVg6NT">Discord</a>
+    ·
+          <a href="https://twitter.com/ariga_io/">Twitter</a>
+     ·
+          <a href="https://github.com/ariga/atlas/issues">Report a Bug</a>
+    ·
+          <a href="https://github.com/ariga/atlas/discussions">Request a Feature</a>
+     </p>
+</div>
 
-# Atlas -  A Database Toolkit
-[![Discord](https://img.shields.io/discord/930720389120794674?label=discord&logo=discord&style=flat-square&logoColor=white)](https://discord.gg/zZ6sWVg6NT)
 
-<img width="50%" align="right" style="display: block; margin:40px auto;"
-     src="https://atlasgo.io/uploads/images/gopher.png"/>
 
 Atlas CLI is an open source tool that helps developers manage their database schemas by applying modern DevOps principles. Contrary to existing tools, Atlas intelligently plans schema migrations for you. Atlas users can use the [Atlas DDL](https://atlasgo.io/concepts/ddl#hcl) (data definition language) to describe their desired database schema and use the command-line tool to plan and apply the migrations to their systems.
 
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index 41b7f515..f3e7338f 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -3,7 +3,7 @@ module ariga.io/atlas/cmd/atlas
 go 1.18
 
 require (
-	ariga.io/atlas v0.6.4-0.20220830111134-f2d233c99aa1
+	ariga.io/atlas v0.6.4-0.20220831192411-1e893f70cbe8
 	entgo.io/ent v0.11.1
 	github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d
 	github.com/auxten/postgresql-parser v1.0.1
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index 3339fe86..e13c7eb2 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -1,5 +1,7 @@
 ariga.io/atlas v0.6.4-0.20220830111134-f2d233c99aa1 h1:/8U7e/qiVREcJHKltZptJoa10gltLcUBVlcqU8gk1V8=
 ariga.io/atlas v0.6.4-0.20220830111134-f2d233c99aa1/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
+ariga.io/atlas v0.6.4-0.20220831192411-1e893f70cbe8 h1:BmgIAqhnFoem+8/jhVAwm+YDRdfeMgcrbOxf/6+3Teg=
+ariga.io/atlas v0.6.4-0.20220831192411-1e893f70cbe8/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 entgo.io/ent v0.11.1 h1:im67R+2W3Nee2bNS2YnoYz8oAF0Qz4AOlIvKRIAEISY=
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index fe8ce892..f2524166 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -7,7 +7,7 @@ replace ariga.io/atlas => ../../
 replace ariga.io/atlas/cmd/atlas => ../../cmd/atlas
 
 require (
-	ariga.io/atlas v0.6.4-0.20220830111134-f2d233c99aa1
+	ariga.io/atlas v0.6.4-0.20220831192411-1e893f70cbe8
 	entgo.io/ent v0.11.1
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/hashicorp/hcl/v2 v2.13.0
@@ -20,7 +20,7 @@ require (
 )
 
 require (
-	ariga.io/atlas/cmd/atlas v0.6.3 // indirect
+	ariga.io/atlas/cmd/atlas v0.6.4-0.20220831192411-1e893f70cbe8 // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
diff --git a/sql/migrate/dir.go b/sql/migrate/dir.go
index c611d693..42356c15 100644
--- a/sql/migrate/dir.go
+++ b/sql/migrate/dir.go
@@ -171,8 +171,22 @@ func (f LocalFile) Version() string {
 	return strings.SplitN(strings.TrimSuffix(f.n, ".sql"), "_", 2)[0]
 }
 
-// Stmts implements Scanner.Stmts. It reads migration file line-by-line and expects a statement to be one line only.
+// Stmts returns the SQL statement exists in the local file.
 func (f LocalFile) Stmts() ([]string, error) {
+	s, err := stmts(string(f.b))
+	if err != nil {
+		return nil, err
+	}
+	stmts := make([]string, len(s))
+	for i := range s {
+		stmts[i] = s[i].Text
+	}
+	return stmts, nil
+}
+
+// StmtDecls returns the all statement declarations exist
+// in the local file.
+func (f LocalFile) StmtDecls() ([]*Stmt, error) {
 	return stmts(string(f.b))
 }
 
diff --git a/sql/migrate/lex.go b/sql/migrate/lex.go
index 4e946df3..6842e07e 100644
--- a/sql/migrate/lex.go
+++ b/sql/migrate/lex.go
@@ -13,10 +13,37 @@ import (
 	"unicode/utf8"
 )
 
+// Stmt represents a scanned statement text along with its
+// position in the file and associated comments group.
+type Stmt struct {
+	Text     string   // statement text
+	Comments []string // associated comments
+}
+
+// Directive returns all directive comments with the given name.
+// See: pkg.go.dev/cmd/compile#hdr-Compiler_Directives.
+func (s *Stmt) Directive(name string) (ds []string) {
+	for _, c := range s.Comments {
+		switch {
+		case strings.HasPrefix(c, "/*") && !strings.Contains(c, "\n"):
+			if d, ok := directive(strings.TrimSuffix(c, "*/"), name, "/*"); ok {
+				ds = append(ds, d)
+			}
+		default:
+			for _, p := range []string{"#", "--", "-- "} {
+				if d, ok := directive(c, name, p); ok {
+					ds = append(ds, d)
+				}
+			}
+		}
+	}
+	return
+}
+
 // stmts provides a generic implementation for extracting
 // SQL statements from the given file contents.
-func stmts(input string) ([]string, error) {
-	var stmts []string
+func stmts(input string) ([]*Stmt, error) {
+	var stmts []*Stmt
 	l, err := newLex(input)
 	if err != nil {
 		return nil, err
@@ -34,11 +61,12 @@ func stmts(input string) ([]string, error) {
 }
 
 type lex struct {
-	input string
-	pos   int    // current position.
-	width int    // size of latest rune.
-	depth int    // depth of parentheses.
-	delim string // configured delimiter.
+	input    string
+	pos      int      // current position
+	width    int      // size of latest rune
+	depth    int      // depth of parentheses
+	delim    string   // configured delimiter
+	comments []string // collected comments
 }
 
 const (
@@ -64,51 +92,47 @@ func newLex(input string) (*lex, error) {
 	return l, nil
 }
 
-func (l *lex) stmt() (stmt string, err error) {
-	defer func() {
-		l.input = l.input[l.pos:]
-		l.pos = 0
-		// Trim custom delimiter.
-		if l.delim != delimiter {
-			stmt = strings.TrimSuffix(stmt, l.delim)
-		}
-		stmt = strings.TrimSpace(stmt)
-	}()
+func (l *lex) stmt() (*Stmt, error) {
+	var text string
 	// Trim trailing whitespace.
 	l.skipSpaces()
+Scan:
 	for {
 		switch r := l.next(); {
 		case r == eos:
 			if l.depth > 0 {
-				return "", errors.New("unclosed parentheses")
+				return nil, errors.New("unclosed parentheses")
 			}
 			if l.pos > 0 {
-				return l.input, nil
+				text = l.input
+				break Scan
 			}
-			return "", io.EOF
+			return nil, io.EOF
 		case r == '(':
 			l.depth++
 		case r == ')':
 			if l.depth == 0 {
-				return "", fmt.Errorf("unexpected ')' at position %d", l.pos)
+				return nil, fmt.Errorf("unexpected ')' at position %d", l.pos)
 			}
 			l.depth--
 		case r == '\'', r == '"', r == '`':
 			if err := l.skipQuote(r); err != nil {
-				return "", err
+				return nil, err
 			}
 		// Delimiters take precedence over comments.
 		case strings.HasPrefix(l.input[l.pos-l.width:], l.delim) && l.depth == 0:
 			l.pos += len(l.delim) - l.width
-			return l.input[:l.pos], nil
+			text = l.input[:l.pos]
+			break Scan
 		case r == '#':
-			l.skipComment("#", "\n")
+			l.comment("#", "\n")
 		case r == '-' && l.next() == '-':
-			l.skipComment("--", "\n")
+			l.comment("--", "\n")
 		case r == '/' && l.next() == '*':
-			l.skipComment("/*", "*/")
+			l.comment("/*", "*/")
 		}
 	}
+	return l.emit(text), nil
 }
 
 func (l *lex) next() rune {
@@ -121,6 +145,13 @@ func (l *lex) next() rune {
 	return r
 }
 
+func (l *lex) pick() rune {
+	p, w := l.pos, l.width
+	r := l.next()
+	l.pos, l.width = p, w
+	return r
+}
+
 func (l *lex) skipQuote(quote rune) error {
 	for {
 		switch r := l.next(); {
@@ -134,23 +165,42 @@ func (l *lex) skipQuote(quote rune) error {
 	}
 }
 
-func (l *lex) skipComment(left, right string) {
+func (l *lex) comment(left, right string) {
 	i := strings.Index(l.input[l.pos:], right)
 	// Not a comment.
 	if i == -1 {
 		return
 	}
-	// If we did not scan any statement
-	// characters, it can be skipped.
-	if l.pos == len(left) {
-		l.input = l.input[l.pos+i+len(right):]
-		l.pos = 0
-		l.skipSpaces()
-	} else {
+	// If the comment reside inside a statement, collect it.
+	if l.pos != len(left) {
 		l.pos += i + len(right)
+		return
+	}
+	// If we did not scan any statement characters, it
+	// can be skipped and stored in the comments group.
+	l.comments = append(l.comments, l.input[:l.pos+i+len(right)])
+	l.input = l.input[l.pos+i+len(right):]
+	l.pos = 0
+	// Double \n separate the comments group from the statement.
+	if strings.HasPrefix(l.input, "\n\n") || right == "\n" && strings.HasPrefix(l.input, "\n") {
+		l.comments = nil
 	}
+	l.skipSpaces()
 }
 
 func (l *lex) skipSpaces() {
 	l.input = strings.TrimLeftFunc(l.input, unicode.IsSpace)
 }
+
+func (l *lex) emit(text string) *Stmt {
+	s := &Stmt{Text: text, Comments: l.comments}
+	l.input = l.input[l.pos:]
+	l.pos = 0
+	l.comments = nil
+	// Trim custom delimiter.
+	if l.delim != delimiter {
+		s.Text = strings.TrimSuffix(s.Text, l.delim)
+	}
+	s.Text = strings.TrimSpace(s.Text)
+	return s
+}


Test patch:
diff --git a/sql/migrate/lex_test.go b/sql/migrate/lex_test.go
index ac93d968..ececa4f5 100644
--- a/sql/migrate/lex_test.go
+++ b/sql/migrate/lex_test.go
@@ -27,3 +27,60 @@ func TestLocalFile_Stmts(t *testing.T) {
 		require.Equal(t, string(buf), strings.Join(stmts, "\n-- end --\n"))
 	}
 }
+
+func TestLocalFile_StmtDecls(t *testing.T) {
+	f := NewLocalFile("f", []byte(`
+-- test
+cmd1;
+
+-- hello
+-- world
+cmd2;
+
+-- skip
+-- this
+# comment
+
+/* Skip this as well */
+
+# Skip this
+/* one */
+
+# command
+cmd3;
+
+/* comment1 */
+/* comment2 */
+cmd4;
+
+--atlas:nolint
+-- atlas:nolint destructive
+cmd5;
+
+#atlas:lint error
+/*atlas:nolint DS101*/
+/* atlas:lint not a directive */
+/*
+atlas:lint not a directive
+*/
+cmd6;
+`))
+	stmts, err := f.StmtDecls()
+	require.NoError(t, err)
+	require.Len(t, stmts, 6)
+	require.Equal(t, "cmd1;", stmts[0].Text)
+	require.Equal(t, []string{"-- test\n"}, stmts[0].Comments)
+	require.Equal(t, "cmd2;", stmts[1].Text)
+	require.Equal(t, []string{"-- hello\n", "-- world\n"}, stmts[1].Comments)
+	require.Equal(t, "cmd3;", stmts[2].Text)
+	require.Equal(t, []string{"# command\n"}, stmts[2].Comments)
+	require.Equal(t, "cmd4;", stmts[3].Text)
+	require.Equal(t, []string{"/* comment1 */", "/* comment2 */"}, stmts[3].Comments)
+	require.Equal(t, "cmd5;", stmts[4].Text)
+	require.Equal(t, []string{"--atlas:nolint\n", "-- atlas:nolint destructive\n"}, stmts[4].Comments)
+	require.Equal(t, []string{"", "destructive"}, stmts[4].Directive("nolint"))
+	require.Equal(t, "cmd6;", stmts[5].Text)
+	require.Equal(t, []string{"#atlas:lint error\n", "/*atlas:nolint DS101*/", "/* atlas:lint not a directive */", "/*\natlas:lint not a directive\n*/"}, stmts[5].Comments)
+	require.Equal(t, []string{"error"}, stmts[5].Directive("lint"))
+	require.Equal(t, []string{"DS101"}, stmts[5].Directive("nolint"))
+}