Skip to content
This repository was archived by the owner on Jan 28, 2021. It is now read-only.

Commit e98fa12

Browse files
authored
Suggest similar table/column/indexes names on missing errors (#685)
Suggest similar table/column/indexes names on missing errors
2 parents e0e8b6a + c9347c3 commit e98fa12

10 files changed

+242
-21
lines changed

Diff for: ARCHITECTURE.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ There are two authentication methods:
9595
- **None:** no authentication needed.
9696
- **Native:** authentication performed with user and password. Read, write or all permissions can be specified for those users. It can also be configured using a JSON file.
9797

98+
## `internal/similartext`
99+
100+
Contains a function to `Find` the most similar name from an
101+
array to a given one using the Levenshtein distance algorithm. Used for suggestions on errors.
102+
98103
## `internal/regex`
99104

100105
go-mysql-server has multiple regular expression engines, such as oniguruma and the standard Go regexp engine. In this package, a common interface for regular expression engines is defined.
@@ -134,4 +139,4 @@ After parsing, the obtained execution plan is analyzed using the analyzer define
134139

135140
If indexes can be used, the analyzer will transform the query so it uses indexes reading from the drivers in `sql/index` (in this case `sql/index/pilosa` because there is only one driver).
136141

137-
Once the plan is analyzed, it will be executed recursively from the top of the tree to the bottom to obtain the results and they will be sent back to the client using the MySQL wire protocol.
142+
Once the plan is analyzed, it will be executed recursively from the top of the tree to the bottom to obtain the results and they will be sent back to the client using the MySQL wire protocol.

Diff for: internal/similartext/similartext.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package similartext
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
)
8+
9+
func min(a, b int) int {
10+
if a < b {
11+
return a
12+
}
13+
return b
14+
}
15+
16+
// DistanceForStrings returns the edit distance between source and target.
17+
// It has a runtime proportional to len(source) * len(target) and memory use
18+
// proportional to len(target).
19+
// Taken (simplified, for strings and with default options) from:
20+
// https://github.com/texttheater/golang-levenshtein
21+
func distanceForStrings(source, target string) int {
22+
height := len(source) + 1
23+
width := len(target) + 1
24+
matrix := make([][]int, 2)
25+
26+
for i := 0; i < 2; i++ {
27+
matrix[i] = make([]int, width)
28+
matrix[i][0] = i
29+
}
30+
for j := 1; j < width; j++ {
31+
matrix[0][j] = j
32+
}
33+
34+
for i := 1; i < height; i++ {
35+
cur := matrix[i%2]
36+
prev := matrix[(i-1)%2]
37+
cur[0] = i
38+
for j := 1; j < width; j++ {
39+
delCost := prev[j] + 1
40+
matchSubCost := prev[j-1]
41+
if source[i-1] != target[j-1] {
42+
matchSubCost += 2
43+
}
44+
insCost := cur[j-1] + 1
45+
cur[j] = min(delCost, min(matchSubCost, insCost))
46+
}
47+
}
48+
return matrix[(height-1)%2][width-1]
49+
}
50+
51+
// MaxDistanceIgnored is the maximum Levenshtein distance from which
52+
// we won't consider a string similar at all and thus will be ignored.
53+
var DistanceSkipped = 3
54+
55+
// Find returns a string with suggestions for name(s) in `names`
56+
// similar to the string `src` until a max distance of `DistanceSkipped`.
57+
func Find(names []string, src string) string {
58+
if len(src) == 0 {
59+
return ""
60+
}
61+
62+
minDistance := -1
63+
matchMap := make(map[int][]string)
64+
65+
for _, name := range names {
66+
dist := distanceForStrings(name, src)
67+
if dist >= DistanceSkipped {
68+
continue
69+
}
70+
71+
if minDistance == -1 || dist < minDistance {
72+
minDistance = dist
73+
}
74+
75+
matchMap[dist] = append(matchMap[dist], name)
76+
}
77+
78+
if len(matchMap) == 0 {
79+
return ""
80+
}
81+
82+
return fmt.Sprintf(", maybe you mean %s?",
83+
strings.Join(matchMap[minDistance], " or "))
84+
}
85+
86+
// FindFromMap does the same as Find but taking a map instead
87+
// of a string array as first argument.
88+
func FindFromMap(names interface{}, src string) string {
89+
rnames := reflect.ValueOf(names)
90+
if rnames.Kind() != reflect.Map {
91+
panic("Implementation error: non map used as first argument " +
92+
"to FindFromMap")
93+
}
94+
95+
t := rnames.Type()
96+
if t.Key().Kind() != reflect.String {
97+
panic("Implementation error: non string key for map used as " +
98+
"first argument to FindFromMap")
99+
}
100+
101+
var namesList []string
102+
for _, kv := range rnames.MapKeys() {
103+
namesList = append(namesList, kv.String())
104+
}
105+
106+
return Find(namesList, src)
107+
}

Diff for: internal/similartext/similartext_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package similartext
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestFind(t *testing.T) {
10+
require := require.New(t)
11+
12+
var names []string
13+
res := Find(names, "")
14+
require.Empty(res)
15+
16+
names = []string{"foo", "bar", "aka", "ake"}
17+
res = Find(names, "baz")
18+
require.Equal(", maybe you mean bar?", res)
19+
20+
res = Find(names, "")
21+
require.Empty(res)
22+
23+
res = Find(names, "foo")
24+
require.Equal(", maybe you mean foo?", res)
25+
26+
res = Find(names, "willBeTooDifferent")
27+
require.Empty(res)
28+
29+
res = Find(names, "aki")
30+
require.Equal(", maybe you mean aka or ake?", res)
31+
}
32+
33+
func TestFindFromMap(t *testing.T) {
34+
require := require.New(t)
35+
36+
var names map[string]int
37+
res := FindFromMap(names, "")
38+
require.Empty(res)
39+
40+
names = map[string]int {
41+
"foo": 1,
42+
"bar": 2,
43+
}
44+
res = FindFromMap(names, "baz")
45+
require.Equal(", maybe you mean bar?", res)
46+
47+
res = FindFromMap(names, "")
48+
require.Empty(res)
49+
50+
res = FindFromMap(names, "foo")
51+
require.Equal(", maybe you mean foo?", res)
52+
}

Diff for: sql/analyzer/resolve_columns.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import (
55
"sort"
66
"strings"
77

8-
errors "gopkg.in/src-d/go-errors.v1"
8+
"gopkg.in/src-d/go-errors.v1"
99
"gopkg.in/src-d/go-mysql-server.v0/sql"
1010
"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
1111
"gopkg.in/src-d/go-mysql-server.v0/sql/plan"
1212
"gopkg.in/src-d/go-vitess.v1/vt/sqlparser"
13+
"gopkg.in/src-d/go-mysql-server.v0/internal/similartext"
1314
)
1415

1516
func checkAliases(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) {
@@ -202,7 +203,12 @@ func qualifyColumns(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error)
202203
}
203204

204205
if _, ok := tables[col.Table()]; !ok {
205-
return nil, sql.ErrTableNotFound.New(col.Table())
206+
if len(tables) == 0 {
207+
return nil, sql.ErrTableNotFound.New(col.Table())
208+
}
209+
210+
similar := similartext.FindFromMap(tables, col.Table())
211+
return nil, sql.ErrTableNotFound.New(col.Table() + similar)
206212
}
207213
}
208214

@@ -406,11 +412,16 @@ func resolveColumns(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error)
406412
return &deferredColumn{uc}, nil
407413

408414
default:
415+
if len(colMap) == 0 {
416+
return nil, ErrColumnNotFound.New(uc.Name())
417+
}
418+
409419
if table != "" {
410420
return nil, ErrColumnTableNotFound.New(uc.Table(), uc.Name())
411421
}
412422

413-
return nil, ErrColumnNotFound.New(uc.Name())
423+
similar := similartext.FindFromMap(colMap, uc.Name())
424+
return nil, ErrColumnNotFound.New(uc.Name() + similar)
414425
}
415426
}
416427

Diff for: sql/catalog.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
"sync"
77

8+
"gopkg.in/src-d/go-mysql-server.v0/internal/similartext"
9+
810
"gopkg.in/src-d/go-errors.v1"
911
)
1012

@@ -93,14 +95,21 @@ type Databases []Database
9395

9496
// Database returns the Database with the given name if it exists.
9597
func (d Databases) Database(name string) (Database, error) {
98+
99+
if len(d) == 0 {
100+
return nil, ErrDatabaseNotFound.New(name)
101+
}
102+
96103
name = strings.ToLower(name)
104+
var dbNames []string
97105
for _, db := range d {
98106
if strings.ToLower(db.Name()) == name {
99107
return db, nil
100108
}
109+
dbNames = append(dbNames, db.Name())
101110
}
102-
103-
return nil, ErrDatabaseNotFound.New(name)
111+
similar := similartext.Find(dbNames, name)
112+
return nil, ErrDatabaseNotFound.New(name + similar)
104113
}
105114

106115
// Add adds a new database.
@@ -118,6 +127,10 @@ func (d Databases) Table(dbName string, tableName string) (Table, error) {
118127
tableName = strings.ToLower(tableName)
119128

120129
tables := db.Tables()
130+
if len(tables) == 0 {
131+
return nil, ErrTableNotFound.New(tableName)
132+
}
133+
121134
// Try to get the table by key, but if the name is not the same,
122135
// then use the slow path and iterate over all tables comparing
123136
// the name.
@@ -129,7 +142,8 @@ func (d Databases) Table(dbName string, tableName string) (Table, error) {
129142
}
130143
}
131144

132-
return nil, ErrTableNotFound.New(tableName)
145+
similar := similartext.FindFromMap(tables, tableName)
146+
return nil, ErrTableNotFound.New(tableName + similar)
133147
}
134148

135149
return table, nil

Diff for: sql/catalog_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ func TestCatalogDatabase(t *testing.T) {
4949
mydb := mem.NewDatabase("foo")
5050
c.AddDatabase(mydb)
5151

52+
db, err = c.Database("flo")
53+
require.EqualError(err, "database not found: flo, maybe you mean foo?")
54+
require.Nil(db)
55+
5256
db, err = c.Database("foo")
5357
require.NoError(err)
5458
require.Equal(mydb, db)
@@ -73,6 +77,10 @@ func TestCatalogTable(t *testing.T) {
7377
mytable := mem.NewTable("bar", nil)
7478
db.AddTable("bar", mytable)
7579

80+
table, err = c.Table("foo", "baz")
81+
require.EqualError(err, "table not found: baz, maybe you mean bar?")
82+
require.Nil(table)
83+
7684
table, err = c.Table("foo", "bar")
7785
require.NoError(err)
7886
require.Equal(mytable, table)

Diff for: sql/functionregistry.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sql
22

33
import (
44
"gopkg.in/src-d/go-errors.v1"
5+
"gopkg.in/src-d/go-mysql-server.v0/internal/similartext"
56
)
67

78
// ErrFunctionAlreadyRegistered is thrown when a function is already registered
@@ -203,9 +204,13 @@ func (r FunctionRegistry) MustRegister(fn ...Function) {
203204

204205
// Function returns a function with the given name.
205206
func (r FunctionRegistry) Function(name string) (Function, error) {
207+
if len(r) == 0 {
208+
return nil, ErrFunctionNotFound.New(name)
209+
}
210+
206211
if fn, ok := r[name]; ok {
207212
return fn, nil
208213
}
209-
210-
return nil, ErrFunctionNotFound.New(name)
214+
similar := similartext.FindFromMap(r, name)
215+
return nil, ErrFunctionNotFound.New(name + similar)
211216
}

Diff for: sql/index.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sql
22

33
import (
4+
"gopkg.in/src-d/go-mysql-server.v0/internal/similartext"
45
"io"
56
"strings"
67
"sync"
@@ -548,6 +549,13 @@ func (r *IndexRegistry) AddIndex(
548549
func (r *IndexRegistry) DeleteIndex(db, id string, force bool) (<-chan struct{}, error) {
549550
r.mut.RLock()
550551
var key indexKey
552+
553+
if len(r.indexes) == 0 {
554+
return nil, ErrIndexNotFound.New(id)
555+
}
556+
557+
var indexNames []string
558+
551559
for k, idx := range r.indexes {
552560
if strings.ToLower(id) == idx.ID() {
553561
if !force && !r.CanRemoveIndex(idx) {
@@ -558,11 +566,13 @@ func (r *IndexRegistry) DeleteIndex(db, id string, force bool) (<-chan struct{},
558566
key = k
559567
break
560568
}
569+
indexNames = append(indexNames, idx.ID())
561570
}
562571
r.mut.RUnlock()
563572

564573
if key.id == "" {
565-
return nil, ErrIndexNotFound.New(id)
574+
similar := similartext.Find(indexNames, id)
575+
return nil, ErrIndexNotFound.New(id + similar)
566576
}
567577

568578
var done = make(chan struct{}, 1)

Diff for: sql/plan/drop_index.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package plan
22

33
import (
4-
errors "gopkg.in/src-d/go-errors.v1"
4+
"gopkg.in/src-d/go-errors.v1"
5+
"gopkg.in/src-d/go-mysql-server.v0/internal/similartext"
56
"gopkg.in/src-d/go-mysql-server.v0/sql"
67
)
78

@@ -51,9 +52,15 @@ func (d *DropIndex) RowIter(ctx *sql.Context) (sql.RowIter, error) {
5152
return nil, ErrTableNotNameable.New()
5253
}
5354

54-
table, ok := db.Tables()[n.Name()]
55+
tables := db.Tables()
56+
table, ok := tables[n.Name()]
5557
if !ok {
56-
return nil, sql.ErrTableNotFound.New(n.Name())
58+
if len(tables) == 0 {
59+
return nil, sql.ErrTableNotFound.New(n.Name())
60+
}
61+
62+
similar := similartext.FindFromMap(tables, n.Name())
63+
return nil, sql.ErrTableNotFound.New(n.Name() + similar)
5764
}
5865

5966
index := d.Catalog.Index(db.Name(), d.Name)

0 commit comments

Comments
 (0)