Repository: ariga/atlas
Pull request #1588 (2023-04-15 09:53:10)

sql/migrate: increase error verbosity


Patch:
diff --git a/sql/migrate/dir.go b/sql/migrate/dir.go
index feb9b7b6..da994851 100644
--- a/sql/migrate/dir.go
+++ b/sql/migrate/dir.go
@@ -565,7 +565,6 @@ func ArchiveDir(dir Dir) ([]byte, error) {
 	var buf bytes.Buffer
 	tw := tar.NewWriter(&buf)
 	defer tw.Close()
-
 	sumF, err := dir.Open(HashFileName)
 	if err != nil && !errors.Is(err, fs.ErrNotExist) {
 		return nil, err
diff --git a/sql/migrate/lex.go b/sql/migrate/lex.go
index 7a453eb0..b43674e4 100644
--- a/sql/migrate/lex.go
+++ b/sql/migrate/lex.go
@@ -84,7 +84,7 @@ func newLex(input string) (*lex, error) {
 		}
 		parts := strings.SplitN(input, "\n", 2)
 		if len(parts) == 1 {
-			return nil, fmt.Errorf("no input found after delimiter %q", d)
+			return nil, l.error(l.pos, "no input found after delimiter %q", d)
 		}
 		l.input = parts[1]
 	}
@@ -96,8 +96,8 @@ var reDollarQuote = regexp.MustCompile(`^\$([A-Za-zÈ-ÿ_][\wÈ-ÿ]*)*\$`)
 
 func (l *lex) stmt() (*Stmt, error) {
 	var (
-		depth int
-		text  string
+		depth, openingPos int
+		text              string
 	)
 	l.skipSpaces()
 Scan:
@@ -106,7 +106,7 @@ Scan:
 		case r == eos:
 			switch {
 			case depth > 0:
-				return nil, errors.New("unclosed parentheses")
+				return nil, l.error(openingPos, "unclosed '('")
 			case l.pos > 0:
 				text = l.input
 				break Scan
@@ -114,10 +114,13 @@ Scan:
 				return nil, io.EOF
 			}
 		case r == '(':
+			if depth == 0 {
+				openingPos = l.pos
+			}
 			depth++
 		case r == ')':
 			if depth == 0 {
-				return nil, fmt.Errorf("unexpected ')' at position %d", l.pos)
+				return nil, l.error(l.pos, "unexpected ')'")
 			}
 			depth--
 		case r == '\'', r == '"', r == '`':
@@ -174,10 +177,11 @@ func (l *lex) addPos(p int) {
 }
 
 func (l *lex) skipQuote(quote rune) error {
+	pos := l.pos
 	for {
 		switch r := l.next(); {
 		case r == eos:
-			return fmt.Errorf("unclosed quote %q", quote)
+			return l.error(pos, "unclosed quote %q", quote)
 		case r == '\\':
 			l.next()
 		case r == quote:
@@ -189,7 +193,7 @@ func (l *lex) skipQuote(quote rune) error {
 func (l *lex) skipDollarQuote() error {
 	m := reDollarQuote.FindString(l.input[l.pos-1:])
 	if m == "" {
-		return fmt.Errorf("unexpected dollar quote at position %d", l.pos)
+		return l.error(l.pos, "unexpected dollar quote")
 	}
 	l.addPos(len(m) - 1)
 	for {
@@ -197,7 +201,7 @@ func (l *lex) skipDollarQuote() error {
 		case r == eos:
 			// Fail only if a delimiter was not set.
 			if l.delim == "" {
-				return errors.New("unclosed dollar-quoted string")
+				return l.error(l.pos, "unclosed dollar-quoted string")
 			}
 			return nil
 		case r == '$' && strings.HasPrefix(l.input[l.pos-1:], m):
@@ -281,3 +285,18 @@ func (l *lex) setDelim(d string) error {
 	l.delim = strings.NewReplacer(`\n`, "\n", `\r`, "\r", `\t`, "\t").Replace(d)
 	return nil
 }
+
+func (l *lex) error(pos int, format string, args ...any) error {
+	format = "%d:%d: " + format
+	var (
+		s    = l.input[:pos]
+		col  = strings.LastIndex(s, "\n")
+		line = 1 + strings.Count(s, "\n")
+	)
+	if line == 1 {
+		col = pos
+	} else {
+		col = pos - col - 1
+	}
+	return fmt.Errorf(format, append([]any{line, col}, args...)...)
+}


Test patch:
diff --git a/sql/migrate/lex_test.go b/sql/migrate/lex_test.go
index a2158231..799086f2 100644
--- a/sql/migrate/lex_test.go
+++ b/sql/migrate/lex_test.go
@@ -105,3 +105,62 @@ cmd7;
 	require.Equal(t, "cmd7;", stmts[7].Text)
 	require.Equal(t, []string{""}, stmts[7].Directive("nolint"))
 }
+
+func TestLex_Errors(t *testing.T) {
+	for _, tt := range []struct {
+		name, stmt, err string
+	}{
+		{
+			name: "unclosed single at 1:1",
+			stmt: "'this quote is unclosed at 1:1",
+			err:  "1:1: unclosed quote '\\''",
+		},
+		{
+			name: "unclosed single at 1:6",
+			stmt: "12345'this quote is unclosed at pos 7",
+			err:  "1:6: unclosed quote '\\''",
+		},
+		{
+			name: "unclosed single at EOS",
+			stmt: "unclosed '",
+			err:  "1:10: unclosed quote '\\''",
+		},
+		{
+			name: "unclosed double at 1:1",
+			stmt: "\"unclosed double",
+			err:  "1:1: unclosed quote '\"'",
+		},
+		{
+			name: "unclosed double at 2:2",
+			stmt: "unclosed double at 2:2\n \"",
+			err:  "2:2: unclosed quote '\"'",
+		},
+		{
+			name: "unclosed double at 5:5",
+			stmt: "unclosed double at 2:2\n\n\n\n1234\"",
+			err:  "5:5: unclosed quote '\"'",
+		},
+		{
+			name: "unclosed parentheses at 1:1",
+			stmt: "(unclosed parentheses",
+			err:  "1:1: unclosed '('",
+		},
+		{
+			name: "unclosed parentheses at 1:3",
+			stmt: "()(unclosed parentheses",
+			err:  "1:3: unclosed '('",
+		},
+		{
+			name: "unexpected parentheses at 1:5",
+			stmt: "1234)6789",
+			err:  "1:5: unexpected ')'",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			l, err := newLex(tt.stmt)
+			require.NoError(t, err)
+			_, err = l.stmt()
+			require.EqualError(t, err, tt.err)
+		})
+	}
+}