Skip to content

Commit 0276143

Browse files
cmoogkyleconroy
authored andcommitted
improves mysql parsing error messages
- additional support for position in errors - adds position expression for schema validation errors
1 parent 7b3ac18 commit 0276143

File tree

5 files changed

+121
-29
lines changed

5 files changed

+121
-29
lines changed

internal/cmd/cmd.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ var genCmd = &cobra.Command{
139139
q, err := mysql.GeneratePkg(name, pkg.Schema, pkg.Queries, settings)
140140
if err != nil {
141141
fmt.Fprintf(os.Stderr, "# package %s\n", name)
142-
fmt.Fprintf(os.Stderr, "error parsing file: %s\n", err)
142+
if parserErr, ok := err.(*dinosql.ParserErr); ok {
143+
for _, fileErr := range parserErr.Errs {
144+
fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err)
145+
}
146+
} else {
147+
fmt.Fprintf(os.Stderr, "error parsing schema: %s\n", err)
148+
}
143149
errored = true
144150
continue
145151
}

internal/mysql/errors.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package mysql
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strconv"
7+
)
8+
9+
func locFromSyntaxErr(errMessage error) (int, error) {
10+
matcher := regexp.MustCompile("position ([0-9]*)")
11+
results := matcher.FindStringSubmatch(errMessage.Error())
12+
if len(results) > 0 {
13+
return strconv.Atoi(results[1])
14+
}
15+
return 0, fmt.Errorf("failed to find position integer in parser error message")
16+
}
17+
18+
func nearStrFromSyntaxErr(errMessage error) (string, error) {
19+
matcher := regexp.MustCompile("near '(.*)'")
20+
results := matcher.FindStringSubmatch(errMessage.Error())
21+
if len(results) > 0 {
22+
return results[1], nil
23+
}
24+
return "", fmt.Errorf("failed to find parser 'near' message")
25+
}
26+
27+
type PositionedErr struct {
28+
Pos int
29+
Err error
30+
}
31+
32+
func (e PositionedErr) Error() string {
33+
return e.Err.Error()
34+
}

internal/mysql/errors_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package mysql
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
"vitess.io/vitess/go/vt/sqlparser"
8+
)
9+
10+
func TestSyntaxErr(t *testing.T) {
11+
tokenizer := sqlparser.NewStringTokenizer("SELEC T id FROM users;")
12+
expectedLocation := 6
13+
expectedNear := "SELEC"
14+
15+
_, parseErr := sqlparser.ParseNextStrictDDL(tokenizer)
16+
if parseErr == nil {
17+
t.Errorf("Tokenizer failed to error on invalid MySQL syntax")
18+
}
19+
20+
location, err := locFromSyntaxErr(parseErr)
21+
if err != nil {
22+
t.Errorf("failed to parse location from sqlparser syntax error message: %v", err)
23+
} else if location != expectedLocation {
24+
t.Errorf("parsed incorrect location from sqlparser syntax error message: %v", cmp.Diff(expectedLocation, location))
25+
}
26+
27+
near, err := nearStrFromSyntaxErr(parseErr)
28+
if err != nil {
29+
t.Errorf("failed to parse 'nearby' chars from sqlparser syntax error message: %v", err)
30+
} else if near != expectedNear {
31+
t.Errorf("parse incorrect 'nearby' chars from sqlparser syntax error message: %v", cmp.Diff(expectedNear, near))
32+
}
33+
}

internal/mysql/parse.go

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,35 @@ func parsePath(sqlPath string, inPkg string, s *Schema, settings dinosql.Generat
3131
return nil, err
3232
}
3333

34+
parseErrors := dinosql.ParserErr{}
35+
3436
parsedQueries := []*Query{}
3537
for _, filename := range files {
3638
blob, err := ioutil.ReadFile(filename)
3739
if err != nil {
38-
return nil, fmt.Errorf("Failed to read file [%v]: %w", filename, err)
40+
parseErrors.Add(filename, "", 0, err)
3941
}
4042
contents := dinosql.RemoveRollbackStatements(string(blob))
4143
if err != nil {
42-
return nil, fmt.Errorf("Failed to read contents of file [%v]: %w", filename, err)
44+
parseErrors.Add(filename, "", 0, err)
45+
continue
4346
}
4447
queries, err := parseContents(filename, contents, s, settings)
4548
if err != nil {
46-
return nil, fmt.Errorf("Failed to parse contents of file [%v]: %w", filename, err)
49+
if positionedErr, ok := err.(PositionedErr); ok {
50+
parseErrors.Add(filename, contents, positionedErr.Pos, err)
51+
} else {
52+
parseErrors.Add(filename, contents, 0, err)
53+
}
54+
continue
4755
}
4856
parsedQueries = append(parsedQueries, queries...)
4957
}
5058

59+
if len(parseErrors.Errs) > 0 {
60+
return nil, &parseErrors
61+
}
62+
5163
return &Result{
5264
Queries: parsedQueries,
5365
Schema: s,
@@ -64,12 +76,20 @@ func parseContents(filename, contents string, s *Schema, settings dinosql.Genera
6476
if err == io.EOF {
6577
break
6678
} else if err != nil {
67-
return nil, err
79+
parsedLoc, locErr := locFromSyntaxErr(err)
80+
if locErr != nil {
81+
parsedLoc = start // next best guess of the error location
82+
}
83+
near, nearErr := nearStrFromSyntaxErr(err)
84+
if nearErr != nil {
85+
return nil, PositionedErr{parsedLoc, fmt.Errorf("syntax error")}
86+
}
87+
return nil, PositionedErr{parsedLoc, fmt.Errorf("syntax error at or near '%s'", near)}
6888
}
6989
query := contents[start : t.Position-1]
7090
result, err := parseQueryString(q, query, s, settings)
7191
if err != nil {
72-
return nil, fmt.Errorf("Failed to parse query in filepath [%v]: %w", filename, err)
92+
return nil, PositionedErr{start, err}
7393
}
7494
start = t.Position
7595
if result == nil {
@@ -87,26 +107,25 @@ func parseQueryString(tree sqlparser.Statement, query string, s *Schema, setting
87107
case *sqlparser.Select:
88108
selectQuery, err := parseSelect(tree, query, s, settings)
89109
if err != nil {
90-
return nil, fmt.Errorf("Failed to parse SELECT query: %w", err)
110+
return nil, err
91111
}
92112
parsedQuery = selectQuery
93113
case *sqlparser.Insert:
94114
insert, err := parseInsert(tree, query, s, settings)
95115
if err != nil {
96-
return nil, fmt.Errorf("Failed to parse INSERT query: %w", err)
116+
return nil, err
97117
}
98118
parsedQuery = insert
99119
case *sqlparser.Update:
100120
update, err := parseUpdate(tree, query, s, settings)
101121
if err != nil {
102-
return nil, fmt.Errorf("Failed to parse UPDATE query: %w", err)
122+
return nil, err
103123
}
104124
parsedQuery = update
105125
case *sqlparser.Delete:
106126
delete, err := parseDelete(tree, query, s, settings)
107-
delete.SchemaLookup = nil
108127
if err != nil {
109-
return nil, fmt.Errorf("Failed to parse DELETE query: %w", err)
128+
return nil, err
110129
}
111130
parsedQuery = delete
112131
case *sqlparser.DDL:
@@ -118,28 +137,28 @@ func parseQueryString(tree sqlparser.Statement, query string, s *Schema, setting
118137
}
119138
paramsReplacedQuery, err := replaceParamStrs(sqlparser.String(tree), parsedQuery.Params)
120139
if err != nil {
121-
return nil, fmt.Errorf("Failed to replace param variables in query string: %w", err)
140+
return nil, fmt.Errorf("failed to replace param variables in query string: %w", err)
122141
}
123142
parsedQuery.SQL = paramsReplacedQuery
124143
return parsedQuery, nil
125144
}
126145

127146
func (q *Query) parseNameAndCmd() error {
128147
if q == nil {
129-
return fmt.Errorf("Cannot parse name and cmd from null query")
148+
return fmt.Errorf("cannot parse name and cmd from null query")
130149
}
131150
_, comments := sqlparser.SplitMarginComments(q.SQL)
132151
err := q.parseLeadingComment(comments.Leading)
133152
if err != nil {
134-
return fmt.Errorf("Failed to parse leading comment %w", err)
153+
return fmt.Errorf("failed to parse leading comment %w", err)
135154
}
136155
return nil
137156
}
138157

139158
func parseSelect(tree *sqlparser.Select, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) {
140159
tableAliasMap, err := parseFrom(tree.From, false)
141160
if err != nil {
142-
return nil, fmt.Errorf("Failed to parse table name alias's: %w", err)
161+
return nil, fmt.Errorf("failed to parse table name alias's: %w", err)
143162
}
144163
defaultTableName := getDefaultTable(tableAliasMap)
145164

@@ -205,7 +224,7 @@ func parseFrom(from sqlparser.TableExprs, isLeftJoined bool) (FromTables, error)
205224
case *sqlparser.AliasedTableExpr:
206225
name, ok := v.Expr.(sqlparser.TableName)
207226
if !ok {
208-
return nil, fmt.Errorf("Failed to parse AliasedTableExpr name: %v", spew.Sdump(v))
227+
return nil, fmt.Errorf("failed to parse AliasedTableExpr name: %v", spew.Sdump(v))
209228
}
210229
t := FromTable{
211230
TrueName: name.Name.String(),
@@ -232,7 +251,7 @@ func parseFrom(from sqlparser.TableExprs, isLeftJoined bool) (FromTables, error)
232251
}
233252
return right, nil
234253
default:
235-
return nil, fmt.Errorf("Failed to parse table expr: %v", spew.Sdump(v))
254+
return nil, fmt.Errorf("failed to parse table expr: %v", spew.Sdump(v))
236255
}
237256
}
238257
return tables, nil
@@ -251,7 +270,7 @@ func getDefaultTable(tableAliasMap FromTables) string {
251270
func parseUpdate(node *sqlparser.Update, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) {
252271
tableAliasMap, err := parseFrom(node.TableExprs, false)
253272
if err != nil {
254-
return nil, fmt.Errorf("Failed to parse table name alias's: %w", err)
273+
return nil, fmt.Errorf("failed to parse table name alias's: %w", err)
255274
}
256275
defaultTable := getDefaultTable(tableAliasMap)
257276
if err != nil {
@@ -267,7 +286,7 @@ func parseUpdate(node *sqlparser.Update, query string, s *Schema, settings dinos
267286
}
268287
colDfn, err := s.getColType(col, tableAliasMap, defaultTable)
269288
if err != nil {
270-
return nil, fmt.Errorf("Failed to determine type of a parameter's column: %w", err)
289+
return nil, fmt.Errorf("failed to determine type of a parameter's column: %w", err)
271290
}
272291
originalParamName := string(newValue.Val)
273292
param := Param{
@@ -280,7 +299,7 @@ func parseUpdate(node *sqlparser.Update, query string, s *Schema, settings dinos
280299

281300
whereParams, err := paramsInWhereExpr(node.Where.Expr, s, tableAliasMap, defaultTable, settings)
282301
if err != nil {
283-
return nil, fmt.Errorf("Failed to parse params from WHERE expression: %w", err)
302+
return nil, fmt.Errorf("failed to parse params from WHERE expression: %w", err)
284303
}
285304

286305
parsedQuery := Query{
@@ -342,7 +361,7 @@ func parseInsert(node *sqlparser.Insert, query string, s *Schema, settings dinos
342361
func parseDelete(node *sqlparser.Delete, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) {
343362
tableAliasMap, err := parseFrom(node.TableExprs, false)
344363
if err != nil {
345-
return nil, fmt.Errorf("Failed to parse table name alias's: %w", err)
364+
return nil, fmt.Errorf("failed to parse table name alias's: %w", err)
346365
}
347366
defaultTableName := getDefaultTable(tableAliasMap)
348367
if err != nil {
@@ -412,7 +431,7 @@ func parseSelectAliasExpr(exprs sqlparser.SelectExprs, s *Schema, tableAliasMap
412431
case *sqlparser.ColName:
413432
res, err := s.getColType(v, tableAliasMap, defaultTable)
414433
if err != nil {
415-
panic(fmt.Sprintf("Column not found in schema: %v", err))
434+
return nil, err
416435
}
417436
if hasAlias {
418437
res.Name = expr.As // applys the alias
@@ -458,11 +477,11 @@ func GeneratePkg(pkgName, schemaPath, querysPath string, settings dinosql.Genera
458477
s := NewSchema()
459478
_, err := parsePath(schemaPath, pkgName, s, settings)
460479
if err != nil {
461-
return nil, fmt.Errorf("schema failure: %w", err)
480+
return nil, err
462481
}
463482
result, err := parsePath(querysPath, pkgName, s, settings)
464483
if err != nil {
465-
return nil, fmt.Errorf("query failure: %w", err)
484+
return nil, err
466485
}
467486
return result, nil
468487
}

internal/mysql/schema.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func (s *Schema) getColType(col *sqlparser.ColName, tableAliasMap FromTables, de
2424
if !col.Qualifier.IsEmpty() {
2525
realTable, ok := tableAliasMap[col.Qualifier.Name.String()]
2626
if !ok {
27-
return nil, fmt.Errorf("Column qualifier [%v] not found in table alias map", col.Qualifier.Name.String())
27+
return nil, fmt.Errorf("column qualifier \"%v\" not found in query", col.Qualifier.Name.String())
2828
}
2929
colDfn, err := s.schemaLookup(realTable.TrueName, col.Name.String())
3030
if err != nil {
@@ -37,7 +37,7 @@ func (s *Schema) getColType(col *sqlparser.ColName, tableAliasMap FromTables, de
3737
return &colDfnCopy, nil
3838
}
3939
if defaultTableName == "" {
40-
return nil, fmt.Errorf("Column reference [%v] is ambiguous -- Add a qualifier", col.Name.String())
40+
return nil, fmt.Errorf("column reference \"%v\" is ambiguous, consider adding a qualifier", col.Name.String())
4141
}
4242
colDfn, err := s.schemaLookup(defaultTableName, col.Name.String())
4343
if err != nil {
@@ -54,7 +54,7 @@ func (s *Schema) Add(ddl *sqlparser.DDL) {
5454
case "create":
5555
name := ddl.Table.Name.String()
5656
if ddl.TableSpec == nil {
57-
panic(fmt.Sprintf("Failed to parse table [%v] schema.", name))
57+
panic(fmt.Sprintf("failed to parse table \"%s\" schema.", name))
5858
}
5959
s.tables[name] = ddl.TableSpec.Columns
6060
}
@@ -63,7 +63,7 @@ func (s *Schema) Add(ddl *sqlparser.DDL) {
6363
func (s *Schema) schemaLookup(table string, col string) (*sqlparser.ColumnDefinition, error) {
6464
cols, ok := s.tables[table]
6565
if !ok {
66-
return nil, fmt.Errorf("Table [%v] not found in Schema", table)
66+
return nil, fmt.Errorf("table \"%s\" not found in schema", table)
6767
}
6868

6969
for _, colDef := range cols {
@@ -72,5 +72,5 @@ func (s *Schema) schemaLookup(table string, col string) (*sqlparser.ColumnDefini
7272
}
7373
}
7474

75-
return nil, fmt.Errorf("Column [%v] not found in table [%v]", col, table)
75+
return nil, fmt.Errorf("column \"%s\" not found in table \"%s\"", col, table)
7676
}

0 commit comments

Comments
 (0)