From b820870f17f04f3e4b74bbfae76a380116f2baa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Thu, 17 Oct 2019 17:33:06 +0200 Subject: [PATCH 01/33] Parse CREATE VIEW statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/parse.go | 7 +- sql/parse/parse_test.go | 1 - sql/parse/util.go | 110 ++++++++++++++++++++++++ sql/parse/views.go | 71 ++++++++++++++++ sql/plan/create_view.go | 180 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 sql/parse/views.go create mode 100644 sql/plan/create_view.go diff --git a/sql/parse/parse.go b/sql/parse/parse.go index f4dfe6f0a5..4b229c126f 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -36,6 +36,7 @@ var ( var ( describeTablesRegex = regexp.MustCompile(`^(describe|desc)\s+table\s+(.*)`) createIndexRegex = regexp.MustCompile(`^create\s+index\s+`) + createViewRegex = regexp.MustCompile(`^create\s+(or\s+replace\s+)?view\s+`) dropIndexRegex = regexp.MustCompile(`^drop\s+index\s+`) showIndexRegex = regexp.MustCompile(`^show\s+(index|indexes|keys)\s+(from|in)\s+\S+\s*`) showCreateRegex = regexp.MustCompile(`^show create\s+\S+\s*`) @@ -47,7 +48,6 @@ var ( unlockTablesRegex = regexp.MustCompile(`^unlock\s+tables$`) lockTablesRegex = regexp.MustCompile(`^lock\s+tables\s`) setRegex = regexp.MustCompile(`^set\s+`) - createViewRegex = regexp.MustCompile(`^create\s+view\s+`) ) // These constants aren't exported from vitess for some reason. This could be removed if we changed this. @@ -82,6 +82,8 @@ func Parse(ctx *sql.Context, query string) (sql.Node, error) { return parseDescribeTables(lowerQuery) case createIndexRegex.MatchString(lowerQuery): return parseCreateIndex(ctx, s) + case createViewRegex.MatchString(lowerQuery): + return parseCreateView(ctx, s) case dropIndexRegex.MatchString(lowerQuery): return parseDropIndex(s) case showIndexRegex.MatchString(lowerQuery): @@ -104,9 +106,6 @@ func Parse(ctx *sql.Context, query string) (sql.Node, error) { return parseLockTables(ctx, s) case setRegex.MatchString(lowerQuery): s = fixSetQuery(s) - case createViewRegex.MatchString(lowerQuery): - // CREATE VIEW parses as a CREATE DDL statement with an empty table spec - return nil, ErrUnsupportedFeature.New("CREATE VIEW") } stmt, err := sqlparser.Parse(s) diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go index b66dca9435..664b713fd5 100644 --- a/sql/parse/parse_test.go +++ b/sql/parse/parse_test.go @@ -1296,7 +1296,6 @@ var fixturesErrors = map[string]*errors.Kind{ `SELECT INTERVAL 1 DAY + INTERVAL 1 DAY`: ErrUnsupportedSyntax, `SELECT '2018-05-01' + (INTERVAL 1 DAY + INTERVAL 1 DAY)`: ErrUnsupportedSyntax, `SELECT AVG(DISTINCT foo) FROM b`: ErrUnsupportedSyntax, - `CREATE VIEW view1 AS SELECT x FROM t1 WHERE x>0`: ErrUnsupportedFeature, } func TestParseErrors(t *testing.T) { diff --git a/sql/parse/util.go b/sql/parse/util.go index bfb358b1d5..fa156fb500 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -308,3 +308,113 @@ func expectQuote(r *bufio.Reader) error { return nil } + +func maybe(matched *bool, str string) parseFunc { + return func (rd *bufio.Reader) error { + *matched = false + strLength := len(str) + + data, err := rd.Peek(strLength) + if err != nil { + // If there are not enough runes, what we expected was not there, which + // is not an error per se. + if len(data) < strLength { + return nil + } + + return err + } + + if strings.ToLower(string(data)) == str { + _, err := rd.Discard(strLength) + if err != nil { + return err + } + + *matched = true + return nil + } + + return nil + } +} + +func multiMaybe(matched *bool, strings ...string) parseFunc { + return func (rd *bufio.Reader) error { + *matched = false + first := true + for _, str := range strings { + if err := maybe(matched, str)(rd); err != nil { + return err + } + + if !*matched { + if first { + return nil + } + + // TODO: add actual string parsed + return errUnexpectedSyntax.New(str, "smth else") + } + + first = false + + if err := skipSpaces(rd); err != nil { + return err + } + } + *matched = true + return nil + } +} + +// Read a list of strings separated by the specified separator, with a rune +// indicating the opening of the list and another one specifying its closing. +// For example, readList('(', ',', ')', list) parses "(uno, dos,tres)" and +// populates list with the array of strings ["uno", "dos", "tres"] +// If the opening is not found, do not advance the reader +func maybeList(opening, separator, closing rune, list []string) parseFunc { + return func(rd *bufio.Reader) error { + r, _, err := rd.ReadRune() + if err != nil { + return err + } + + if r != opening { + rd.UnreadRune() + return nil + } + + for { + var newItem string + err := parseFuncs{ + skipSpaces, + readIdent(&newItem), + skipSpaces, + }.exec(rd) + + if err != nil { + return err + } + + r, _, err := rd.ReadRune() + if err != nil { + return err + } + + switch r { + case closing: + list = append(list, newItem) + return nil + case separator: + list = append(list, newItem) + continue + default: + return errUnexpectedSyntax.New( + fmt.Sprintf("%v or %v", separator, closing), + string(r), + ) + } + } + } +} diff --git a/sql/parse/views.go b/sql/parse/views.go new file mode 100644 index 0000000000..844d1ba29f --- /dev/null +++ b/sql/parse/views.go @@ -0,0 +1,71 @@ +package parse + +import ( + "bufio" + "strings" + // "io" + + "github.com/src-d/go-mysql-server/sql" + "github.com/src-d/go-mysql-server/sql/plan" + + "gopkg.in/src-d/go-errors.v1" + "vitess.io/vitess/go/vt/sqlparser" +) + +var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT query") + +// Parses +// CREATE [OR REPLACE] VIEW view_name [(col1, col2, ...)] AS select_statement +// and returns a NewCreateView node in case of success +func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { + r := bufio.NewReader(strings.NewReader(s)) + + + var ( + viewName, subquery string + columns []string + isReplace bool + ) + + err := parseFuncs{ + expect("create"), + skipSpaces, + multiMaybe(&isReplace, "or", "replace"), + skipSpaces, + expect("view"), + skipSpaces, + readIdent(&viewName), + skipSpaces, + maybeList('(', ',', ')', columns), + skipSpaces, + expect("as"), + skipSpaces, + readRemaining(&subquery), + checkEOF, + }.exec(r) + + if err != nil { + return nil, err + } + + subqueryStatement, err := sqlparser.Parse(subquery) + if err != nil { + return nil, err + } + + selectStatement, ok := subqueryStatement.(*sqlparser.Select) + if !ok { + return nil, ErrMalformedCreateView.New(subqueryStatement) + } + + subqueryNode, err := convertSelect(ctx, selectStatement) + if err != nil { + return nil, err + } + + subqueryAlias := plan.NewSubqueryAlias(viewName, subqueryNode) + + return plan.NewCreateView( + sql.UnresolvedDatabase(""), viewName, columns, subqueryAlias, + ), nil +} diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go new file mode 100644 index 0000000000..a2e57095ce --- /dev/null +++ b/sql/plan/create_view.go @@ -0,0 +1,180 @@ +package plan + +import ( + "fmt" + "strings" + // "time" + + // opentracing "github.com/opentracing/opentracing-go" + // otlog "github.com/opentracing/opentracing-go/log" + // "github.com/sirupsen/logrus" + "github.com/src-d/go-mysql-server/sql" + // "github.com/src-d/go-mysql-server/sql/expression" + // errors "gopkg.in/src-d/go-errors.v1" +) + +type CreateView struct { + Database sql.Database + Name string + Columns []string + Definition *SubqueryAlias +} + +func NewCreateView( + database sql.Database, + name string, + columns []string, + definition *SubqueryAlias, +) *CreateView { + return &CreateView{ + Database: database, + Name: name, + Columns: columns, + Definition: definition, + } +} + +// Children implements the Node interface. +func (c *CreateView) Children() []sql.Node { return nil } + +// Resolved implements the Node interface. +func (c *CreateView) Resolved() bool { + _, ok := c.Database.(sql.UnresolvedDatabase) + return !ok && c.Definition.Resolved() +} +// +// func getIndexableTable(t sql.Table) (sql.IndexableTable, error) { +// switch t := t.(type) { +// case sql.IndexableTable: +// return t, nil +// case sql.TableWrapper: +// return getIndexableTable(t.Underlying()) +// default: +// return nil, ErrNotIndexable.New() +// } +// } +// +// func getChecksumable(t sql.Table) sql.Checksumable { +// switch t := t.(type) { +// case sql.Checksumable: +// return t +// case sql.TableWrapper: +// return getChecksumable(t.Underlying()) +// default: +// return nil +// } +// } +// +// RowIter implements the Node interface. +func (c *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { + // TODO: add it to the register + + // table, ok := c.Table.(*ResolvedTable) + // if !ok { + // return nil, ErrNotIndexable.New() + // } + // + // indexable, err := getIndexableTable(table.Table) + // if err != nil { + // return nil, err + // } + // + // var driver sql.IndexDriver + // if c.Driver == "" { + // driver = c.Catalog.DefaultIndexDriver() + // } else { + // driver = c.Catalog.IndexDriver(c.Driver) + // } + // + // if driver == nil { + // return nil, ErrInvalidIndexDriver.New(c.Driver) + // } + // + // columns, exprs, err := getColumnsAndPrepareExpressions(c.Exprs) + // if err != nil { + // return nil, err + // } + // + // for _, e := range exprs { + // if e.Type() == sql.Blob || e.Type() == sql.JSON { + // return nil, ErrExprTypeNotIndexable.New(e, e.Type()) + // } + // } + // + // if ch := getChecksumable(table.Table); ch != nil { + // c.Config[sql.ChecksumKey], err = ch.Checksum() + // if err != nil { + // return nil, err + // } + // } + // + // index, err := driver.Create( + // c.CurrentDatabase, + // table.Name(), + // c.Name, + // exprs, + // c.Config, + // ) + // if err != nil { + // return nil, err + // } + // + // iter, err := indexable.IndexKeyValues(ctx, columns) + // if err != nil { + // return nil, err + // } + // + // iter = &evalPartitionKeyValueIter{ + // ctx: ctx, + // columns: columns, + // exprs: exprs, + // iter: iter, + // } + // + // created, ready, err := c.Catalog.AddIndex(index) + // if err != nil { + // return nil, err + // } + // + // log := logrus.WithFields(logrus.Fields{ + // "id": index.ID(), + // "driver": index.Driver(), + // }) + // + // createIndex := func() { + // c.createIndex(ctx, log, driver, index, iter, created, ready) + // c.Catalog.ProcessList.Done(ctx.Pid()) + // } + // + // log.WithField("async", c.Async).Info("starting to save the index") + // + // if c.Async { + // go createIndex() + // } else { + // createIndex() + // } + + return sql.RowsToRowIter(), nil +} + +// Schema implements the Node interface. +func (c *CreateView) Schema() sql.Schema { return nil } + +func (create *CreateView) String() string { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("CreateView(%s)", create.Name) + _ = pr.WriteChildren( + fmt.Sprintf("Columns (%s)", strings.Join(create.Columns, ", ")), + fmt.Sprintf("As (%s)", create.Definition.String()), + ) + return pr.String() +} + +// WithChildren implements the Node interface. +func (c *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { + if len(children) != 0 { + return nil, sql.ErrInvalidChildrenNumber.New(c, len(children), 0) + } + + return c, nil +} From 079e4193ef74139123b6b996ec763a63aa38481c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 18 Oct 2019 16:29:04 +0200 Subject: [PATCH 02/33] Resolve Catalog in CreateView node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/assign_catalog.go | 4 + sql/index.go | 2 +- sql/parse/parse.go | 2 +- sql/parse/util.go | 4 +- sql/parse/views.go | 5 +- sql/plan/create_view.go | 164 ++++++--------------------------- 6 files changed, 39 insertions(+), 142 deletions(-) diff --git a/sql/analyzer/assign_catalog.go b/sql/analyzer/assign_catalog.go index 0a8f76ee81..1044514549 100644 --- a/sql/analyzer/assign_catalog.go +++ b/sql/analyzer/assign_catalog.go @@ -60,6 +60,10 @@ func assignCatalog(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) nc := *node nc.Catalog = a.Catalog return &nc, nil + case *plan.CreateView: + nc := *node + nc.Catalog = a.Catalog + return &nc, nil default: return n, nil } diff --git a/sql/index.go b/sql/index.go index c673341ca5..827763f1df 100644 --- a/sql/index.go +++ b/sql/index.go @@ -512,7 +512,7 @@ func exprListsEqual(a, b []string) bool { // marked as creating, so nobody can't register two indexes with the same // expression or id while the other is still being created. // When something is sent through the returned channel, it means the index has -// finished it's creation and will be marked as ready. +// finished its creation and will be marked as ready. // Another channel is returned to notify the user when the index is ready. func (r *IndexRegistry) AddIndex( idx Index, diff --git a/sql/parse/parse.go b/sql/parse/parse.go index 4b229c126f..aea61f9906 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -36,7 +36,7 @@ var ( var ( describeTablesRegex = regexp.MustCompile(`^(describe|desc)\s+table\s+(.*)`) createIndexRegex = regexp.MustCompile(`^create\s+index\s+`) - createViewRegex = regexp.MustCompile(`^create\s+(or\s+replace\s+)?view\s+`) + createViewRegex = regexp.MustCompile(`^create\s+(or\s+replace\s+)?view\s+`) dropIndexRegex = regexp.MustCompile(`^drop\s+index\s+`) showIndexRegex = regexp.MustCompile(`^show\s+(index|indexes|keys)\s+(from|in)\s+\S+\s*`) showCreateRegex = regexp.MustCompile(`^show create\s+\S+\s*`) diff --git a/sql/parse/util.go b/sql/parse/util.go index fa156fb500..9e3655fffb 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -310,7 +310,7 @@ func expectQuote(r *bufio.Reader) error { } func maybe(matched *bool, str string) parseFunc { - return func (rd *bufio.Reader) error { + return func(rd *bufio.Reader) error { *matched = false strLength := len(str) @@ -340,7 +340,7 @@ func maybe(matched *bool, str string) parseFunc { } func multiMaybe(matched *bool, strings ...string) parseFunc { - return func (rd *bufio.Reader) error { + return func(rd *bufio.Reader) error { *matched = false first := true for _, str := range strings { diff --git a/sql/parse/views.go b/sql/parse/views.go index 844d1ba29f..f5473ac172 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -20,11 +20,10 @@ var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { r := bufio.NewReader(strings.NewReader(s)) - var ( viewName, subquery string - columns []string - isReplace bool + columns []string + isReplace bool ) err := parseFuncs{ diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index a2e57095ce..4340c8f6a7 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -3,178 +3,72 @@ package plan import ( "fmt" "strings" - // "time" - // opentracing "github.com/opentracing/opentracing-go" - // otlog "github.com/opentracing/opentracing-go/log" - // "github.com/sirupsen/logrus" "github.com/src-d/go-mysql-server/sql" - // "github.com/src-d/go-mysql-server/sql/expression" - // errors "gopkg.in/src-d/go-errors.v1" ) type CreateView struct { - Database sql.Database - Name string - Columns []string - Definition *SubqueryAlias + UnaryNode + Database sql.Database + Name string + Columns []string + Catalog *sql.Catalog } func NewCreateView( - database sql.Database, - name string, + database sql.Database, + name string, columns []string, definition *SubqueryAlias, ) *CreateView { return &CreateView{ - Database: database, - Name: name, - Columns: columns, - Definition: definition, + UnaryNode{Child: definition}, + database, + name, + columns, + nil, } } // Children implements the Node interface. -func (c *CreateView) Children() []sql.Node { return nil } +func (create *CreateView) Children() []sql.Node { + return []sql.Node{create.Child} +} // Resolved implements the Node interface. -func (c *CreateView) Resolved() bool { - _, ok := c.Database.(sql.UnresolvedDatabase) - return !ok && c.Definition.Resolved() +func (create *CreateView) Resolved() bool { + // TOOD: Check whether the database has been resolved + // _, ok := create.Database.(sql.UnresolvedDatabase) + return create.Child.Resolved() } -// -// func getIndexableTable(t sql.Table) (sql.IndexableTable, error) { -// switch t := t.(type) { -// case sql.IndexableTable: -// return t, nil -// case sql.TableWrapper: -// return getIndexableTable(t.Underlying()) -// default: -// return nil, ErrNotIndexable.New() -// } -// } -// -// func getChecksumable(t sql.Table) sql.Checksumable { -// switch t := t.(type) { -// case sql.Checksumable: -// return t -// case sql.TableWrapper: -// return getChecksumable(t.Underlying()) -// default: -// return nil -// } -// } -// + // RowIter implements the Node interface. -func (c *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { +func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { // TODO: add it to the register - // table, ok := c.Table.(*ResolvedTable) - // if !ok { - // return nil, ErrNotIndexable.New() - // } - // - // indexable, err := getIndexableTable(table.Table) - // if err != nil { - // return nil, err - // } - // - // var driver sql.IndexDriver - // if c.Driver == "" { - // driver = c.Catalog.DefaultIndexDriver() - // } else { - // driver = c.Catalog.IndexDriver(c.Driver) - // } - // - // if driver == nil { - // return nil, ErrInvalidIndexDriver.New(c.Driver) - // } - // - // columns, exprs, err := getColumnsAndPrepareExpressions(c.Exprs) - // if err != nil { - // return nil, err - // } - // - // for _, e := range exprs { - // if e.Type() == sql.Blob || e.Type() == sql.JSON { - // return nil, ErrExprTypeNotIndexable.New(e, e.Type()) - // } - // } - // - // if ch := getChecksumable(table.Table); ch != nil { - // c.Config[sql.ChecksumKey], err = ch.Checksum() - // if err != nil { - // return nil, err - // } - // } - // - // index, err := driver.Create( - // c.CurrentDatabase, - // table.Name(), - // c.Name, - // exprs, - // c.Config, - // ) - // if err != nil { - // return nil, err - // } - // - // iter, err := indexable.IndexKeyValues(ctx, columns) - // if err != nil { - // return nil, err - // } - // - // iter = &evalPartitionKeyValueIter{ - // ctx: ctx, - // columns: columns, - // exprs: exprs, - // iter: iter, - // } - // - // created, ready, err := c.Catalog.AddIndex(index) - // if err != nil { - // return nil, err - // } - // - // log := logrus.WithFields(logrus.Fields{ - // "id": index.ID(), - // "driver": index.Driver(), - // }) - // - // createIndex := func() { - // c.createIndex(ctx, log, driver, index, iter, created, ready) - // c.Catalog.ProcessList.Done(ctx.Pid()) - // } - // - // log.WithField("async", c.Async).Info("starting to save the index") - // - // if c.Async { - // go createIndex() - // } else { - // createIndex() - // } - return sql.RowsToRowIter(), nil } // Schema implements the Node interface. -func (c *CreateView) Schema() sql.Schema { return nil } +func (create *CreateView) Schema() sql.Schema { return nil } func (create *CreateView) String() string { pr := sql.NewTreePrinter() _ = pr.WriteNode("CreateView(%s)", create.Name) _ = pr.WriteChildren( fmt.Sprintf("Columns (%s)", strings.Join(create.Columns, ", ")), - fmt.Sprintf("As (%s)", create.Definition.String()), + fmt.Sprintf("As (%s)", create.Child.String()), ) return pr.String() } // WithChildren implements the Node interface. -func (c *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { - if len(children) != 0 { - return nil, sql.ErrInvalidChildrenNumber.New(c, len(children), 0) +func (create *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { + if len(children) != 1 { + return nil, sql.ErrInvalidChildrenNumber.New(create, len(children), 1) } - return c, nil + newCreate := create + newCreate.Child = children[0] + return newCreate, nil } From 06c778550b8d34548603fc5900c4314306b2fdad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 18 Oct 2019 16:44:24 +0200 Subject: [PATCH 03/33] Fix maybeList so it does populate the passed list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 6 +++--- sql/parse/views.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index 9e3655fffb..8601599a7f 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -373,7 +373,7 @@ func multiMaybe(matched *bool, strings ...string) parseFunc { // For example, readList('(', ',', ')', list) parses "(uno, dos,tres)" and // populates list with the array of strings ["uno", "dos", "tres"] // If the opening is not found, do not advance the reader -func maybeList(opening, separator, closing rune, list []string) parseFunc { +func maybeList(opening, separator, closing rune, list *[]string) parseFunc { return func(rd *bufio.Reader) error { r, _, err := rd.ReadRune() if err != nil { @@ -404,10 +404,10 @@ func maybeList(opening, separator, closing rune, list []string) parseFunc { switch r { case closing: - list = append(list, newItem) + *list = append(*list, newItem) return nil case separator: - list = append(list, newItem) + *list = append(*list, newItem) continue default: return errUnexpectedSyntax.New( diff --git a/sql/parse/views.go b/sql/parse/views.go index f5473ac172..0aad809e4c 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -35,7 +35,7 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { skipSpaces, readIdent(&viewName), skipSpaces, - maybeList('(', ',', ')', columns), + maybeList('(', ',', ')', &columns), skipSpaces, expect("as"), skipSpaces, From 34a9f583a1ce1cb6ce908c795e4e99cddc21bc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 22 Oct 2019 15:47:19 +0200 Subject: [PATCH 04/33] Add a registry for VIEWs to the catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/catalog.go | 2 + sql/parse/views.go | 1 - sql/plan/create_view.go | 13 +++---- sql/viewregistry.go | 81 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 sql/viewregistry.go diff --git a/sql/catalog.go b/sql/catalog.go index ee3a48adea..4397ea6569 100644 --- a/sql/catalog.go +++ b/sql/catalog.go @@ -17,6 +17,7 @@ var ErrDatabaseNotFound = errors.NewKind("database not found: %s") type Catalog struct { FunctionRegistry *IndexRegistry + *ViewRegistry *ProcessList *MemoryManager @@ -37,6 +38,7 @@ func NewCatalog() *Catalog { return &Catalog{ FunctionRegistry: NewFunctionRegistry(), IndexRegistry: NewIndexRegistry(), + ViewRegistry: NewViewRegistry(), MemoryManager: NewMemoryManager(ProcessMemory), ProcessList: NewProcessList(), locks: make(sessionLocks), diff --git a/sql/parse/views.go b/sql/parse/views.go index 0aad809e4c..0b84cd2ddf 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -3,7 +3,6 @@ package parse import ( "bufio" "strings" - // "io" "github.com/src-d/go-mysql-server/sql" "github.com/src-d/go-mysql-server/sql/plan" diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 4340c8f6a7..16b3a33b7e 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -9,7 +9,7 @@ import ( type CreateView struct { UnaryNode - Database sql.Database + database sql.Database Name string Columns []string Catalog *sql.Catalog @@ -37,16 +37,15 @@ func (create *CreateView) Children() []sql.Node { // Resolved implements the Node interface. func (create *CreateView) Resolved() bool { - // TOOD: Check whether the database has been resolved - // _, ok := create.Database.(sql.UnresolvedDatabase) - return create.Child.Resolved() + _, ok := create.database.(sql.UnresolvedDatabase) + return !ok && create.Child.Resolved() } // RowIter implements the Node interface. func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { - // TODO: add it to the register + view := sql.View{create.Name, create.Child} - return sql.RowsToRowIter(), nil + return sql.RowsToRowIter(), create.Catalog.ViewRegistry.Register(create.database.Name(), view) } // Schema implements the Node interface. @@ -57,7 +56,7 @@ func (create *CreateView) String() string { _ = pr.WriteNode("CreateView(%s)", create.Name) _ = pr.WriteChildren( fmt.Sprintf("Columns (%s)", strings.Join(create.Columns, ", ")), - fmt.Sprintf("As (%s)", create.Child.String()), + create.Child.String(), ) return pr.String() } diff --git a/sql/viewregistry.go b/sql/viewregistry.go new file mode 100644 index 0000000000..d9cb6d1065 --- /dev/null +++ b/sql/viewregistry.go @@ -0,0 +1,81 @@ +package sql + +import ( + "sync" + + "gopkg.in/src-d/go-errors.v1" +) + +var ErrExistingView = errors.NewKind("the view %s.%s already exists in the registry") +var ErrNonExistingView = errors.NewKind("the view %s.%s does not exist in the registry") + +type View struct { + Name string + Definition Node +} + +type viewKey struct { + dbName, viewName string +} + +type ViewRegistry struct { + mutex sync.RWMutex + views map[viewKey]View +} + +func NewViewRegistry() *ViewRegistry { + return &ViewRegistry{ + views: make(map[viewKey]View), + } +} + +func (registry *ViewRegistry) Register(database string, view View) error { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + key := viewKey{database, view.Name} + + if _, ok := registry.views[key]; ok { + return ErrExistingView.New(database, view.Name) + } + + registry.views[key] = view + return nil +} + +func (registry *ViewRegistry) Delete(databaseName, viewName string) error { + key := viewKey{databaseName, viewName} + + if _, ok := registry.views[key]; !ok { + return ErrNonExistingView.New(databaseName, viewName) + } + + delete(registry.views, key) + return nil +} + +func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) { + registry.mutex.RLock() + defer registry.mutex.RUnlock() + + key := viewKey{databaseName, viewName} + + if view, ok := registry.views[key]; ok { + return &view, nil + } + + return nil, ErrNonExistingView.New(databaseName, viewName) +} + +func (registry *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { + registry.mutex.RLock() + defer registry.mutex.RUnlock() + + for key, value := range registry.views { + if key.dbName == databaseName { + views = append(views, value) + } + } + + return views +} From ef19c5f8a896d0eb38a9e3c1b6705ca892bb2ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 22 Oct 2019 15:48:29 +0200 Subject: [PATCH 05/33] Parse and analyze database-scoped names for views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 61 +++++++++++++++++++++++++++++++++++++++++ sql/parse/views.go | 27 ++++++++++++++---- sql/plan/create_view.go | 12 +++++++- 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index 8601599a7f..5440b0ef62 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -127,6 +127,27 @@ func readLetter(r *bufio.Reader, buf *bytes.Buffer) error { return nil } +func readLetterOrPoint(r *bufio.Reader, buf *bytes.Buffer) error { + ru, _, err := r.ReadRune() + if err != nil { + if err == io.EOF { + return nil + } + + return err + } + + if !unicode.IsLetter(ru) && ru != '.' { + if err := r.UnreadRune(); err != nil { + return err + } + return nil + } + + buf.WriteRune(ru) + return nil +} + func readValidIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { ru, _, err := r.ReadRune() if err != nil { @@ -144,6 +165,23 @@ func readValidIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { return nil } +func readValidScopedIdentRune(r *bufio.Reader, separator rune, buf *bytes.Buffer) error { + ru, _, err := r.ReadRune() + if err != nil { + return err + } + + if !unicode.IsLetter(ru) && !unicode.IsDigit(ru) && ru != '_' && ru != separator { + if err := r.UnreadRune(); err != nil { + return err + } + return io.EOF + } + + buf.WriteRune(ru) + return nil +} + func readValidQuotedIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { bs, err := r.Peek(2) if err != nil { @@ -199,6 +237,29 @@ func readIdent(ident *string) parseFunc { } } +func readScopedIdent(separator rune, idents *[]string) parseFunc { + return func(r *bufio.Reader) error { + var buf bytes.Buffer + if err := readLetter(r, &buf); err != nil { + return err + } + + for { + if err := readValidScopedIdentRune(r, separator, &buf); err == io.EOF { + break + } else if err != nil { + return err + } + } + + *idents = append( + *idents, + strings.Split(strings.ToLower(buf.String()), string(separator))..., + ) + return nil + } +} + func readQuotedIdent(ident *string) parseFunc { return func(r *bufio.Reader) error { var buf bytes.Buffer diff --git a/sql/parse/views.go b/sql/parse/views.go index 0b84cd2ddf..79926eaa08 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -11,18 +11,21 @@ import ( "vitess.io/vitess/go/vt/sqlparser" ) +var ErrMalformedViewName = errors.NewKind("the view name '%s' is not correct") var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT query") // Parses -// CREATE [OR REPLACE] VIEW view_name [(col1, col2, ...)] AS select_statement +// CREATE [OR REPLACE] VIEW [db_name.]view_name [(col1, col2, ...)] AS select_statement // and returns a NewCreateView node in case of success func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { r := bufio.NewReader(strings.NewReader(s)) var ( - viewName, subquery string - columns []string - isReplace bool + databaseName, viewName string + scopedName []string + subquery string + columns []string + isReplace bool ) err := parseFuncs{ @@ -32,7 +35,7 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { skipSpaces, expect("view"), skipSpaces, - readIdent(&viewName), + readScopedIdent('.', &scopedName), skipSpaces, maybeList('(', ',', ')', &columns), skipSpaces, @@ -46,6 +49,18 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { return nil, err } + if len(scopedName) < 1 || len(scopedName) > 2 { + return nil, ErrMalformedViewName.New(strings.Join(scopedName, ".")) + } + + if len(scopedName) == 1 { + viewName = scopedName[0] + } + if len(scopedName) == 2 { + databaseName = scopedName[0] + viewName = scopedName[1] + } + subqueryStatement, err := sqlparser.Parse(subquery) if err != nil { return nil, err @@ -64,6 +79,6 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { subqueryAlias := plan.NewSubqueryAlias(viewName, subqueryNode) return plan.NewCreateView( - sql.UnresolvedDatabase(""), viewName, columns, subqueryAlias, + sql.UnresolvedDatabase(databaseName), viewName, columns, subqueryAlias, ), nil } diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 16b3a33b7e..0e1e5d2eb6 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -12,7 +12,7 @@ type CreateView struct { database sql.Database Name string Columns []string - Catalog *sql.Catalog + Catalog *sql.Catalog } func NewCreateView( @@ -71,3 +71,13 @@ func (create *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { newCreate.Child = children[0] return newCreate, nil } + +func (create *CreateView) Database() sql.Database { + return create.database +} + +func (create *CreateView) WithDatabase(database sql.Database) (sql.Node, error) { + newCreate := *create + newCreate.database = database + return &newCreate, nil +} From 7de54c5e5ae4da47523ecbfa53cced1783ba2b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 22 Oct 2019 15:50:06 +0200 Subject: [PATCH 06/33] Plug views' definition when resolving tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/resolve_tables.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/sql/analyzer/resolve_tables.go b/sql/analyzer/resolve_tables.go index ec495cd362..54024f1bfc 100644 --- a/sql/analyzer/resolve_tables.go +++ b/sql/analyzer/resolve_tables.go @@ -39,17 +39,26 @@ func resolveTables(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) } rt, err := a.Catalog.Table(db, name) - if err != nil { - if sql.ErrTableNotFound.Is(err) && name == dualTableName { + if err == nil { + a.Log("table resolved: %q", t.Name()) + return plan.NewResolvedTable(rt), nil + } + + if sql.ErrTableNotFound.Is(err) { + if name == dualTableName { rt = dualTable name = dualTableName - } else { - return nil, err + + a.Log("table resolved: %q", t.Name()) + return plan.NewResolvedTable(rt), nil } - } - a.Log("table resolved: %q", t.Name()) + if view, err := a.Catalog.ViewRegistry.View(db, name); err == nil { + a.Log("table %q is a view: replacing plans", t.Name()) + return view.Definition, nil + } + } - return plan.NewResolvedTable(rt), nil + return nil, err }) } From 4764cae6618393095a3900ca351c2d7480c72568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 22 Oct 2019 17:32:26 +0200 Subject: [PATCH 07/33] Document new util functions for parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index 5440b0ef62..2a1aa4daca 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -127,6 +127,8 @@ func readLetter(r *bufio.Reader, buf *bytes.Buffer) error { return nil } +// Parses a single rune from the reader and consumes it, copying it to the +// buffer, if it is either a letter or a point func readLetterOrPoint(r *bufio.Reader, buf *bytes.Buffer) error { ru, _, err := r.ReadRune() if err != nil { @@ -165,6 +167,8 @@ func readValidIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { return nil } +// Parses a single rune from the reader and consumes it, copying it to the +// buffer, if is a letter, a digit, an underscore or the specified separator. func readValidScopedIdentRune(r *bufio.Reader, separator rune, buf *bytes.Buffer) error { ru, _, err := r.ReadRune() if err != nil { @@ -237,6 +241,14 @@ func readIdent(ident *string) parseFunc { } } + +// Reads a scoped identifier, populating the specified slice with the different +// parts of the identifier if it is correctly formed. +// A scoped identifier is a sequence of identifiers separated by the specified +// rune in separator. An identifier is a string of runes whose first character +// is a letter and the following ones are either letters, digits or underscores. +// An example of a correctly formed scoped identifier is "dbName.tableName", +// that would populate the slice with the values ["dbName", "tableName"] func readScopedIdent(separator rune, idents *[]string) parseFunc { return func(r *bufio.Reader) error { var buf bytes.Buffer @@ -370,6 +382,8 @@ func expectQuote(r *bufio.Reader) error { return nil } +// Tries to read the specified string, consuming the reader if the string is +// found. The `matched` boolean is set to true if the string is found func maybe(matched *bool, str string) parseFunc { return func(rd *bufio.Reader) error { *matched = false @@ -400,6 +414,9 @@ func maybe(matched *bool, str string) parseFunc { } } +// Tries to read the specified strings, one after the other, separeted by an +// arbitrary number of spaces. It consumes the reader if and only all the +// strings are found. func multiMaybe(matched *bool, strings ...string) parseFunc { return func(rd *bufio.Reader) error { *matched = false @@ -429,11 +446,11 @@ func multiMaybe(matched *bool, strings ...string) parseFunc { } } -// Read a list of strings separated by the specified separator, with a rune +// Reads a list of strings separated by the specified separator, with a rune // indicating the opening of the list and another one specifying its closing. // For example, readList('(', ',', ')', list) parses "(uno, dos,tres)" and // populates list with the array of strings ["uno", "dos", "tres"] -// If the opening is not found, do not advance the reader +// If the opening is not found, this does not consumes any rune from the reader. func maybeList(opening, separator, closing rune, list *[]string) parseFunc { return func(rd *bufio.Reader) error { r, _, err := rd.ReadRune() From 676f862301e1c5b6a5cff02a5c0afe8592241eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Wed, 23 Oct 2019 17:30:31 +0200 Subject: [PATCH 08/33] Ensure that keys in view registry are always lowercase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/viewregistry.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sql/viewregistry.go b/sql/viewregistry.go index d9cb6d1065..724063ef56 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -1,13 +1,16 @@ package sql import ( + "strings" "sync" "gopkg.in/src-d/go-errors.v1" ) -var ErrExistingView = errors.NewKind("the view %s.%s already exists in the registry") -var ErrNonExistingView = errors.NewKind("the view %s.%s does not exist in the registry") +var ( + ErrExistingView = errors.NewKind("the view %s.%s already exists in the registry") + ErrNonExistingView = errors.NewKind("the view %s.%s does not exist in the registry") +) type View struct { Name string @@ -18,6 +21,11 @@ type viewKey struct { dbName, viewName string } +// Creates a viewKey ensuring both names are lowercase +func newViewKey(databaseName, viewName string) viewKey { + return viewKey{strings.ToLower(databaseName), strings.ToLower(viewName)} +} + type ViewRegistry struct { mutex sync.RWMutex views map[viewKey]View @@ -33,7 +41,7 @@ func (registry *ViewRegistry) Register(database string, view View) error { registry.mutex.Lock() defer registry.mutex.Unlock() - key := viewKey{database, view.Name} + key := newViewKey(database, view.Name) if _, ok := registry.views[key]; ok { return ErrExistingView.New(database, view.Name) @@ -43,8 +51,10 @@ func (registry *ViewRegistry) Register(database string, view View) error { return nil } +// Deletes the view specified by the pair {databaseName, viewName}, returning +// an error if it does not exist func (registry *ViewRegistry) Delete(databaseName, viewName string) error { - key := viewKey{databaseName, viewName} + key := newViewKey(databaseName, viewName) if _, ok := registry.views[key]; !ok { return ErrNonExistingView.New(databaseName, viewName) @@ -58,7 +68,7 @@ func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) registry.mutex.RLock() defer registry.mutex.RUnlock() - key := viewKey{databaseName, viewName} + key := newViewKey(databaseName, viewName) if view, ok := registry.views[key]; ok { return &view, nil From 0fd0480bda9429d6b10835b681e99169141f25c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Wed, 23 Oct 2019 17:32:47 +0200 Subject: [PATCH 09/33] Manage OR REPLACE cases in VIEW creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/views.go | 7 ++++++- sql/plan/create_view.go | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/sql/parse/views.go b/sql/parse/views.go index 79926eaa08..9548ce008e 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -53,6 +53,11 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { return nil, ErrMalformedViewName.New(strings.Join(scopedName, ".")) } + // TODO(agarciamontoro): Add support for explicit column names + if len(columns) != 0 { + return nil, ErrUnsupportedSyntax.New("the view creation must not specify explicit column names") + } + if len(scopedName) == 1 { viewName = scopedName[0] } @@ -79,6 +84,6 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { subqueryAlias := plan.NewSubqueryAlias(viewName, subqueryNode) return plan.NewCreateView( - sql.UnresolvedDatabase(databaseName), viewName, columns, subqueryAlias, + sql.UnresolvedDatabase(databaseName), viewName, columns, subqueryAlias, isReplace, ), nil } diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 0e1e5d2eb6..4640b33c52 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -13,6 +13,7 @@ type CreateView struct { Name string Columns []string Catalog *sql.Catalog + IsReplace bool } func NewCreateView( @@ -20,6 +21,7 @@ func NewCreateView( name string, columns []string, definition *SubqueryAlias, + isReplace bool, ) *CreateView { return &CreateView{ UnaryNode{Child: definition}, @@ -27,6 +29,7 @@ func NewCreateView( name, columns, nil, + isReplace, } } @@ -45,6 +48,10 @@ func (create *CreateView) Resolved() bool { func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { view := sql.View{create.Name, create.Child} + if create.IsReplace { + _ = create.Catalog.ViewRegistry.Delete(create.database.Name(), view.Name) + } + return sql.RowsToRowIter(), create.Catalog.ViewRegistry.Register(create.database.Name(), view) } From d71a9a5cd5be1244c8118fcdf36fc581c5982ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Wed, 23 Oct 2019 17:34:53 +0200 Subject: [PATCH 10/33] Test new changes from VIEW implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/assign_catalog_test.go | 8 ++++ sql/analyzer/resolve_tables_test.go | 41 ++++++++++++++++ sql/parse/parse_test.go | 39 +++++++++++++++- sql/parse/util_test.go | 72 +++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 sql/parse/util_test.go diff --git a/sql/analyzer/assign_catalog_test.go b/sql/analyzer/assign_catalog_test.go index 39aed1a4f5..a7fd9dff09 100644 --- a/sql/analyzer/assign_catalog_test.go +++ b/sql/analyzer/assign_catalog_test.go @@ -73,4 +73,12 @@ func TestAssignCatalog(t *testing.T) { ut, ok := node.(*plan.UnlockTables) require.True(ok) require.Equal(c, ut.Catalog) + + mockSubquery := plan.NewSubqueryAlias("mock", plan.NewResolvedTable(tbl)) + mockView := plan.NewCreateView(db, "", nil, mockSubquery, false) + node, err = f.Apply(sql.NewEmptyContext(), a, mockView) + require.NoError(err) + cv, ok := node.(*plan.CreateView) + require.True(ok) + require.Equal(c, cv.Catalog) } diff --git a/sql/analyzer/resolve_tables_test.go b/sql/analyzer/resolve_tables_test.go index fbc4fcca0e..030986bba1 100644 --- a/sql/analyzer/resolve_tables_test.go +++ b/sql/analyzer/resolve_tables_test.go @@ -91,3 +91,44 @@ func TestResolveTablesNested(t *testing.T) { ) require.Equal(expected, analyzed) } + +func TestResolveViews(t *testing.T) { + require := require.New(t) + + f := getRule("resolve_tables") + + table := memory.NewTable("mytable", sql.Schema{{Name: "i", Type: sql.Int32}}) + db := memory.NewDatabase("mydb") + db.AddTable("mytable", table) + + // Resolved plan that corresponds to query "SELECT i FROM mytable" + subquery := plan.NewProject( + []sql.Expression{ + expression.NewGetFieldWithTable( + 1, sql.Int32, table.Name(), "i", true), + }, + plan.NewResolvedTable(table), + ) + subqueryAlias := plan.NewSubqueryAlias("myview", subquery) + view := sql.View{"myview", subqueryAlias} + + catalog := sql.NewCatalog() + catalog.AddDatabase(db) + catalog.ViewRegistry.Register(db.Name(), view) + + a := NewBuilder(catalog).AddPostAnalyzeRule(f.Name, f.Apply).Build() + + var notAnalyzed sql.Node = plan.NewUnresolvedTable("myview", "") + analyzed, err := f.Apply(sql.NewEmptyContext(), a, notAnalyzed) + require.NoError(err) + require.Equal(subqueryAlias, analyzed) + + notAnalyzed = plan.NewUnresolvedTable("MyVieW", "") + analyzed, err = f.Apply(sql.NewEmptyContext(), a, notAnalyzed) + require.NoError(err) + require.Equal(subqueryAlias, analyzed) + + analyzed, err = f.Apply(sql.NewEmptyContext(), a, subqueryAlias) + require.NoError(err) + require.Equal(subqueryAlias, analyzed) +} diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go index 664b713fd5..88ab0f0de8 100644 --- a/sql/parse/parse_test.go +++ b/sql/parse/parse_test.go @@ -1,8 +1,8 @@ package parse import ( - "math" "testing" + "math" "github.com/src-d/go-mysql-server/sql/expression" "github.com/src-d/go-mysql-server/sql/expression/function/aggregation" @@ -1263,6 +1263,42 @@ var fixtures = map[string]sql.Node{ }, plan.NewUnresolvedTable("dual", ""), ), + `CREATE VIEW myview AS SELECT 1`: plan.NewCreateView( + sql.UnresolvedDatabase(""), + "myview", + nil, + plan.NewSubqueryAlias("myview", + plan.NewProject( + []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, + plan.NewUnresolvedTable("dual", ""), + ), + ), + false, + ), + `CREATE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( + sql.UnresolvedDatabase("mydb"), + "myview", + nil, + plan.NewSubqueryAlias("myview", + plan.NewProject( + []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, + plan.NewUnresolvedTable("dual", ""), + ), + ), + false, + ), + `CREATE OR REPLACE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( + sql.UnresolvedDatabase("mydb"), + "myview", + nil, + plan.NewSubqueryAlias( "myview", + plan.NewProject( + []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, + plan.NewUnresolvedTable("dual", ""), + ), + ), + true, + ), } func TestParse(t *testing.T) { @@ -1296,6 +1332,7 @@ var fixturesErrors = map[string]*errors.Kind{ `SELECT INTERVAL 1 DAY + INTERVAL 1 DAY`: ErrUnsupportedSyntax, `SELECT '2018-05-01' + (INTERVAL 1 DAY + INTERVAL 1 DAY)`: ErrUnsupportedSyntax, `SELECT AVG(DISTINCT foo) FROM b`: ErrUnsupportedSyntax, + `CREATE VIEW myview (col1) AS SELECT 1`: ErrUnsupportedSyntax, } func TestParseErrors(t *testing.T) { diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go new file mode 100644 index 0000000000..2ae006941c --- /dev/null +++ b/sql/parse/util_test.go @@ -0,0 +1,72 @@ +package parse + +import ( + "bufio" + "strings" + "testing" + "bytes" + + "github.com/stretchr/testify/require" +) + +func TestReadLetterOrPoint(t *testing.T) { + testFixtures := []struct { + string string + expectedBuffer string + expectedRemaining string + }{ + { "asd.ASD.ñu", + "asd.ASD.ñu", + "", + }, + { "5anytext", + "", + "5anytext", + }, + { "", + "", + "", + }, + { "as df", + "as", + " df", + }, + { "a.s df", + "a.s", + " df", + }, + { "a.s-", + "a.s", + "-", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.string)) + var buffer bytes.Buffer + + for i := 0; i < len(fixture.string); i++ { + readLetterOrPoint(reader, &buffer) + } + + remaining, _ := reader.ReadString('\n') + require.Equal(t, remaining, fixture.expectedRemaining) + + require.Equal(t, buffer.String(), fixture.expectedBuffer) + } +} + +func TestReadValidScopedIdentRune(t *testing.T) { +} + +func TestReadScopedIdent(t *testing.T) { +} + +func TestMaybe(t *testing.T) { +} + +func TestMultiMaybe(t *testing.T) { +} + +func TestMaybeList(t *testing.T) { +} From 032fa6c749c0b17331227c21841bc28a08446652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Thu, 24 Oct 2019 22:51:21 +0200 Subject: [PATCH 11/33] Improve View and ViewRegistry interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/resolve_tables.go | 2 +- sql/analyzer/resolve_tables_test.go | 2 +- sql/parse/views.go | 2 +- sql/plan/create_view.go | 20 ++++++++++----- sql/viewregistry.go | 40 +++++++++++++++++++++++++---- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/sql/analyzer/resolve_tables.go b/sql/analyzer/resolve_tables.go index 54024f1bfc..0ab516a9c8 100644 --- a/sql/analyzer/resolve_tables.go +++ b/sql/analyzer/resolve_tables.go @@ -55,7 +55,7 @@ func resolveTables(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) if view, err := a.Catalog.ViewRegistry.View(db, name); err == nil { a.Log("table %q is a view: replacing plans", t.Name()) - return view.Definition, nil + return view.Definition(), nil } } diff --git a/sql/analyzer/resolve_tables_test.go b/sql/analyzer/resolve_tables_test.go index 030986bba1..53c0cac328 100644 --- a/sql/analyzer/resolve_tables_test.go +++ b/sql/analyzer/resolve_tables_test.go @@ -110,7 +110,7 @@ func TestResolveViews(t *testing.T) { plan.NewResolvedTable(table), ) subqueryAlias := plan.NewSubqueryAlias("myview", subquery) - view := sql.View{"myview", subqueryAlias} + view := sql.NewView("myview", subqueryAlias) catalog := sql.NewCatalog() catalog.AddDatabase(db) diff --git a/sql/parse/views.go b/sql/parse/views.go index 9548ce008e..30e9d3e5e2 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -15,7 +15,7 @@ var ErrMalformedViewName = errors.NewKind("the view name '%s' is not correct") var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT query") // Parses -// CREATE [OR REPLACE] VIEW [db_name.]view_name [(col1, col2, ...)] AS select_statement +// CREATE [OR REPLACE] VIEW [db_name.]view_name AS select_statement // and returns a NewCreateView node in case of success func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { r := bufio.NewReader(strings.NewReader(s)) diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 4640b33c52..735a4bc7c0 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -9,10 +9,10 @@ import ( type CreateView struct { UnaryNode - database sql.Database - Name string - Columns []string - Catalog *sql.Catalog + database sql.Database + Name string + Columns []string + Catalog *sql.Catalog IsReplace bool } @@ -33,7 +33,13 @@ func NewCreateView( } } -// Children implements the Node interface. +// View returns the view that will be created by this node +func (create *CreateView) View() sql.View { + return sql.NewView(create.Name, create.Child) +} + +// Children implements the Node interface. It returns the Child of the +// CreateView node; i.e., the definition of the view that will be created. func (create *CreateView) Children() []sql.Node { return []sql.Node{create.Child} } @@ -46,10 +52,10 @@ func (create *CreateView) Resolved() bool { // RowIter implements the Node interface. func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { - view := sql.View{create.Name, create.Child} + view := sql.NewView(create.Name, create.Child) if create.IsReplace { - _ = create.Catalog.ViewRegistry.Delete(create.database.Name(), view.Name) + _ = create.Catalog.ViewRegistry.Delete(create.database.Name(), view.Name()) } return sql.RowsToRowIter(), create.Catalog.ViewRegistry.Register(create.database.Name(), view) diff --git a/sql/viewregistry.go b/sql/viewregistry.go index 724063ef56..52e8945ab7 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -8,15 +8,33 @@ import ( ) var ( - ErrExistingView = errors.NewKind("the view %s.%s already exists in the registry") + ErrExistingView = errors.NewKind("the view %s.%s already exists in the registry") ErrNonExistingView = errors.NewKind("the view %s.%s does not exist in the registry") ) +// A View is defined by a Node and has a name type View struct { - Name string - Definition Node + name string + definition Node } +// Creates a View with the specified name and definition +func NewView(name string, definition Node) View { + return View{name, definition} +} + +// Returns the name of the view +func (view *View) Name() string { + return view.name +} + +// Returns the definition of the view +func (view *View) Definition() Node { + return view.definition +} + +// Views are scoped by the databases in which they were defined, so a key in +// the view registry is a pair of names: database and view type viewKey struct { dbName, viewName string } @@ -41,10 +59,10 @@ func (registry *ViewRegistry) Register(database string, view View) error { registry.mutex.Lock() defer registry.mutex.Unlock() - key := newViewKey(database, view.Name) + key := newViewKey(database, view.Name()) if _, ok := registry.views[key]; ok { - return ErrExistingView.New(database, view.Name) + return ErrExistingView.New(database, view.Name()) } registry.views[key] = view @@ -54,6 +72,9 @@ func (registry *ViewRegistry) Register(database string, view View) error { // Deletes the view specified by the pair {databaseName, viewName}, returning // an error if it does not exist func (registry *ViewRegistry) Delete(databaseName, viewName string) error { + registry.mutex.Lock() + defer registry.mutex.Unlock() + key := newViewKey(databaseName, viewName) if _, ok := registry.views[key]; !ok { @@ -77,6 +98,15 @@ func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) return nil, ErrNonExistingView.New(databaseName, viewName) } +// Returns the map of all views in the registry +func (registry *ViewRegistry) AllViews() map[viewKey]View { + registry.mutex.RLock() + defer registry.mutex.RUnlock() + + return registry.views +} + +// Returns an array of all the views registered under the specified database func (registry *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { registry.mutex.RLock() defer registry.mutex.RUnlock() From c2de021462ce587af1170c50eccd057d884f53a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Thu, 24 Oct 2019 22:54:09 +0200 Subject: [PATCH 12/33] Document and test VIEW-related changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/resolve_tables_test.go | 7 + sql/parse/parse_test.go | 76 ++---- sql/parse/util.go | 60 +++-- sql/parse/util_test.go | 398 +++++++++++++++++++++++++++- sql/parse/views_test.go | 77 ++++++ sql/plan/create_view.go | 26 +- sql/plan/create_view_test.go | 95 +++++++ sql/viewregistry.go | 25 +- sql/viewregistry_test.go | 134 ++++++++++ 9 files changed, 799 insertions(+), 99 deletions(-) create mode 100644 sql/parse/views_test.go create mode 100644 sql/plan/create_view_test.go create mode 100644 sql/viewregistry_test.go diff --git a/sql/analyzer/resolve_tables_test.go b/sql/analyzer/resolve_tables_test.go index 53c0cac328..1d00d787a6 100644 --- a/sql/analyzer/resolve_tables_test.go +++ b/sql/analyzer/resolve_tables_test.go @@ -92,6 +92,9 @@ func TestResolveTablesNested(t *testing.T) { require.Equal(expected, analyzed) } +// Tests the resolution of views (ensuring it is case-insensitive), that should +// result in the replacement of the UnresolvedTable with the SubqueryAlias that +// represents the view func TestResolveViews(t *testing.T) { require := require.New(t) @@ -112,22 +115,26 @@ func TestResolveViews(t *testing.T) { subqueryAlias := plan.NewSubqueryAlias("myview", subquery) view := sql.NewView("myview", subqueryAlias) + // Register the view in the catalog catalog := sql.NewCatalog() catalog.AddDatabase(db) catalog.ViewRegistry.Register(db.Name(), view) a := NewBuilder(catalog).AddPostAnalyzeRule(f.Name, f.Apply).Build() + // Check whether the view is resolved and replaced with the subquery var notAnalyzed sql.Node = plan.NewUnresolvedTable("myview", "") analyzed, err := f.Apply(sql.NewEmptyContext(), a, notAnalyzed) require.NoError(err) require.Equal(subqueryAlias, analyzed) + // Ensures that the resolution is case-insensitive notAnalyzed = plan.NewUnresolvedTable("MyVieW", "") analyzed, err = f.Apply(sql.NewEmptyContext(), a, notAnalyzed) require.NoError(err) require.Equal(subqueryAlias, analyzed) + // Ensures that the resolution is idempotent analyzed, err = f.Apply(sql.NewEmptyContext(), a, subqueryAlias) require.NoError(err) require.Equal(subqueryAlias, analyzed) diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go index 88ab0f0de8..1d8b5f49e2 100644 --- a/sql/parse/parse_test.go +++ b/sql/parse/parse_test.go @@ -1,8 +1,8 @@ package parse import ( - "testing" "math" + "testing" "github.com/src-d/go-mysql-server/sql/expression" "github.com/src-d/go-mysql-server/sql/expression/function/aggregation" @@ -55,14 +55,14 @@ var fixtures = map[string]sql.Node{ sql.UnresolvedDatabase(""), "t1", sql.Schema{{ - Name: "a", - Type: sql.Int32, - Nullable: false, + Name: "a", + Type: sql.Int32, + Nullable: false, PrimaryKey: true, }, { - Name: "b", - Type: sql.Text, - Nullable: true, + Name: "b", + Type: sql.Text, + Nullable: true, PrimaryKey: false, }}, ), @@ -70,14 +70,14 @@ var fixtures = map[string]sql.Node{ sql.UnresolvedDatabase(""), "t1", sql.Schema{{ - Name: "a", - Type: sql.Int32, - Nullable: true, + Name: "a", + Type: sql.Int32, + Nullable: true, PrimaryKey: true, }, { - Name: "b", - Type: sql.Text, - Nullable: true, + Name: "b", + Type: sql.Text, + Nullable: true, PrimaryKey: false, }}, ), @@ -85,14 +85,14 @@ var fixtures = map[string]sql.Node{ sql.UnresolvedDatabase(""), "t1", sql.Schema{{ - Name: "a", - Type: sql.Int32, - Nullable: true, + Name: "a", + Type: sql.Int32, + Nullable: true, PrimaryKey: true, }, { - Name: "b", - Type: sql.Text, - Nullable: true, + Name: "b", + Type: sql.Text, + Nullable: true, PrimaryKey: true, }}, ), @@ -1263,42 +1263,6 @@ var fixtures = map[string]sql.Node{ }, plan.NewUnresolvedTable("dual", ""), ), - `CREATE VIEW myview AS SELECT 1`: plan.NewCreateView( - sql.UnresolvedDatabase(""), - "myview", - nil, - plan.NewSubqueryAlias("myview", - plan.NewProject( - []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, - plan.NewUnresolvedTable("dual", ""), - ), - ), - false, - ), - `CREATE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( - sql.UnresolvedDatabase("mydb"), - "myview", - nil, - plan.NewSubqueryAlias("myview", - plan.NewProject( - []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, - plan.NewUnresolvedTable("dual", ""), - ), - ), - false, - ), - `CREATE OR REPLACE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( - sql.UnresolvedDatabase("mydb"), - "myview", - nil, - plan.NewSubqueryAlias( "myview", - plan.NewProject( - []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, - plan.NewUnresolvedTable("dual", ""), - ), - ), - true, - ), } func TestParse(t *testing.T) { @@ -1332,7 +1296,7 @@ var fixturesErrors = map[string]*errors.Kind{ `SELECT INTERVAL 1 DAY + INTERVAL 1 DAY`: ErrUnsupportedSyntax, `SELECT '2018-05-01' + (INTERVAL 1 DAY + INTERVAL 1 DAY)`: ErrUnsupportedSyntax, `SELECT AVG(DISTINCT foo) FROM b`: ErrUnsupportedSyntax, - `CREATE VIEW myview (col1) AS SELECT 1`: ErrUnsupportedSyntax, + `CREATE VIEW myview (col1) AS SELECT 1`: ErrUnsupportedSyntax, } func TestParseErrors(t *testing.T) { diff --git a/sql/parse/util.go b/sql/parse/util.go index 2a1aa4daca..b3464a9881 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -80,6 +80,27 @@ func skipSpaces(r *bufio.Reader) error { } } +// Reads every contiguous space from the reader, populating numSpacesRead with +// the number of spaces read. +func readSpaces(r *bufio.Reader, numSpacesRead *int) error { + *numSpacesRead = 0 + for { + ru, _, err := r.ReadRune() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + if !unicode.IsSpace(ru) { + return r.UnreadRune() + } + *numSpacesRead++ + } +} + func checkEOF(rd *bufio.Reader) error { r, _, err := rd.ReadRune() if err == io.EOF { @@ -241,7 +262,6 @@ func readIdent(ident *string) parseFunc { } } - // Reads a scoped identifier, populating the specified slice with the different // parts of the identifier if it is correctly formed. // A scoped identifier is a sequence of identifiers separated by the specified @@ -264,10 +284,12 @@ func readScopedIdent(separator rune, idents *[]string) parseFunc { } } - *idents = append( - *idents, - strings.Split(strings.ToLower(buf.String()), string(separator))..., - ) + if readString := buf.String(); len(readString) > 0 { + *idents = append( + *idents, + strings.Split(strings.ToLower(readString), string(separator))..., + ) + } return nil } } @@ -414,32 +436,32 @@ func maybe(matched *bool, str string) parseFunc { } } -// Tries to read the specified strings, one after the other, separeted by an -// arbitrary number of spaces. It consumes the reader if and only all the +// Tries to read the specified strings, one after the other, separated by an +// arbitrary number of spaces. It consumes the reader if and only if all the // strings are found. func multiMaybe(matched *bool, strings ...string) parseFunc { return func(rd *bufio.Reader) error { *matched = false - first := true + var read string for _, str := range strings { if err := maybe(matched, str)(rd); err != nil { return err } if !*matched { - if first { - return nil - } - - // TODO: add actual string parsed - return errUnexpectedSyntax.New(str, "smth else") + unreadString(rd, read) + return nil } - first = false - - if err := skipSpaces(rd); err != nil { + var numSpaces int + if err := readSpaces(rd, &numSpaces); err != nil { return err } + + read = read + str + for i := 0; i < numSpaces; i++ { + read = read + " " + } } *matched = true return nil @@ -450,7 +472,9 @@ func multiMaybe(matched *bool, strings ...string) parseFunc { // indicating the opening of the list and another one specifying its closing. // For example, readList('(', ',', ')', list) parses "(uno, dos,tres)" and // populates list with the array of strings ["uno", "dos", "tres"] -// If the opening is not found, this does not consumes any rune from the reader. +// If the opening is not found, this does not consumes any rune from the +// reader. If there is a parsing error after some elements were found, the list +// is partially populated with the correct fields func maybeList(opening, separator, closing rune, list *[]string) parseFunc { return func(rd *bufio.Reader) error { r, _, err := rd.ReadRune() diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go index 2ae006941c..b5675f56e6 100644 --- a/sql/parse/util_test.go +++ b/sql/parse/util_test.go @@ -2,43 +2,58 @@ package parse import ( "bufio" + "bytes" "strings" "testing" - "bytes" "github.com/stretchr/testify/require" ) +// Tests that readLetterOrPoint reads only letters and points, not consuming +// the reader when the rune is not of those kinds func TestReadLetterOrPoint(t *testing.T) { + require := require.New(t) + testFixtures := []struct { - string string - expectedBuffer string + string string + expectedBuffer string expectedRemaining string }{ - { "asd.ASD.ñu", + { + "asd.ASD.ñu", "asd.ASD.ñu", "", }, - { "5anytext", + { + "5anytext", "", "5anytext", }, - { "", + { + "", "", "", }, - { "as df", + { + "as df", "as", " df", }, - { "a.s df", + { + "a.s df", "a.s", " df", }, - { "a.s-", + { + "a.s-", "a.s", "-", }, + { + "a.s_", + "a.s", + "_", + }, } for _, fixture := range testFixtures { @@ -50,23 +65,384 @@ func TestReadLetterOrPoint(t *testing.T) { } remaining, _ := reader.ReadString('\n') - require.Equal(t, remaining, fixture.expectedRemaining) + require.Equal(remaining, fixture.expectedRemaining) - require.Equal(t, buffer.String(), fixture.expectedBuffer) + require.Equal(buffer.String(), fixture.expectedBuffer) } } +// Tests that readValidScopedIdentRune reads a single rune that is either part +// of an identifier or the specified separator. It checks that the function +// does not consume the reader when it encounters any other rune func TestReadValidScopedIdentRune(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + string string + separator rune + expectedBuffer string + expectedRemaining string + }{ + { + "ident_1.ident_2", + '.', + "ident_1.ident_2", + "", + }, + { + "$ident_1.ident_2", + '.', + "", + "$ident_1.ident_2", + }, + { + "", + '.', + "", + "", + }, + { + "ident_1 ident_2", + '.', + "ident_1", + " ident_2", + }, + { + "ident_1 ident_2", + ' ', + "ident_1 ident_2", + "", + }, + { + "ident_1.ident_2 ident_3", + '.', + "ident_1.ident_2", + " ident_3", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.string)) + var buffer bytes.Buffer + + for i := 0; i < len(fixture.string); i++ { + readValidScopedIdentRune(reader, fixture.separator, &buffer) + } + + remaining, _ := reader.ReadString('\n') + require.Equal(remaining, fixture.expectedRemaining) + + require.Equal(buffer.String(), fixture.expectedBuffer) + } } +// Tests that readScopedIdent reads a list of identifiers separated by a user- +// specified rune, populating the passed slice with the identifiers found. func TestReadScopedIdent(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + string string + separator rune + expectedIdents []string + expectedRemaining string + }{ + { + "ident_1.ident_2", + '.', + []string{"ident_1", "ident_2"}, + "", + }, + { + "$ident_1.ident_2", + '.', + nil, + "$ident_1.ident_2", + }, + { + "", + '.', + nil, + "", + }, + { + "ident_1 ident_2", + '.', + []string{"ident_1"}, + " ident_2", + }, + { + "ident_1 ident_2", + ' ', + []string{"ident_1", "ident_2"}, + "", + }, + { + "ident_1.ident_2 ident_3", + '.', + []string{"ident_1", "ident_2"}, + " ident_3", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.string)) + var actualIdents []string + + err := readScopedIdent(fixture.separator, &actualIdents)(reader) + require.NoError(err) + + remaining, _ := reader.ReadString('\n') + require.Equal(fixture.expectedRemaining, remaining) + + require.Equal(fixture.expectedIdents, actualIdents) + } } +// Tests that maybe reads, and consumes, the specified string, if and only if +// it is there, reporting the result in the boolean passed. func TestMaybe(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + input string + maybeString string + expectedMatched bool + expectedRemaining string + }{ + { + "ident_1.ident_2", + "ident_1", + true, + ".ident_2", + }, + { + "ident_1.ident_2", + "random", + false, + "ident_1.ident_2", + }, + { + "ident_1.ident_2", + "ident_1.ident_2", + true, + "", + }, + { + "ident_1", + "ident_1butlonger", + false, + "ident_1", + }, + { + "ident_1", + "", + true, + "ident_1", + }, + { + "", + "", + true, + "", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.input)) + var actualMatched bool + + err := maybe(&actualMatched, fixture.maybeString)(reader) + require.NoError(err) + + remaining, _ := reader.ReadString('\n') + require.Equal(fixture.expectedRemaining, remaining) + + require.Equal(fixture.expectedMatched, actualMatched) + } } +// Tests that multiMaybe reads, and consumes, the list of strings passed if all +// of them are in the reader, reporting the result in the boolean passed. func TestMultiMaybe(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + input string + maybeStrings []string + expectedMatched bool + expectedRemaining string + }{ + { + "unodostres", + []string{"uno", "dos", "tres"}, + true, + "", + }, + { + "uno dos tres", + []string{"uno", "dos", "tres"}, + true, + "", + }, + { + "uno dos tres", + []string{"uno", "dos", "tres"}, + true, + "", + }, + { + "uno dos tres", + []string{"random"}, + false, + "uno dos tres", + }, + { + "uno dos tres", + []string{"uno", "random"}, + false, + "uno dos tres", + }, + { + "uno dos tres", + []string{"uno", "dos", "tres", "cuatro"}, + false, + "uno dos tres", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.input)) + var actualMatched bool + + err := multiMaybe(&actualMatched, fixture.maybeStrings...)(reader) + require.NoError(err) + + remaining, _ := reader.ReadString('\n') + require.Equal(fixture.expectedRemaining, remaining) + + require.Equal(fixture.expectedMatched, actualMatched) + } } +// Tests that maybeList reads the specified list of strings separated by the +// user-specified separator, not consuming the reader if the opening rune is +// not found. It checks that the function populates the list with the found +// strings even if there is an error in the middle of the parsing. func TestMaybeList(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + stringWithList string + openingRune rune + separatorRune rune + closingRune rune + expectedList []string + expectedError bool + }{ + { + "(uno, dos, tres)", + '(', ',', ')', + []string{"uno", "dos", "tres"}, + false, + }, + { + "-uno&dos & tres-", + '-', '&', '-', + []string{"uno", "dos", "tres"}, + false, + }, + { + "-(uno, dos, tres)", + '(', ',', ')', + nil, + false, + }, + { + "(uno, dos,( tres)", + '(', ',', ')', + []string{"uno", "dos"}, + true, + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.stringWithList)) + var actualList []string + + err := maybeList(fixture.openingRune, fixture.separatorRune, fixture.closingRune, &actualList)(reader) + + if fixture.expectedError { + require.Error(err) + } else { + require.NoError(err) + } + + require.Equal(fixture.expectedList, actualList) + } +} + +// Tests that readSpaces consumes all the spaces it ecounters in the reader, +// reporting the number of spaces read to the user through the integer passed. +func TestReadSpaces(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + stringWithSpaces string + runesBeforeSpaces int + expectedNumSpaces int + expectedRemaining string + }{ + { + "one", + 3, 0, + "", + }, + { + "two", + 0, 0, + "two", + }, + { + " three", + 0, 3, + "three", + }, + { + "four four ", + 4, 4, + "four ", + }, + { + "five ", + 4, 5, + "", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.stringWithSpaces)) + var actualNumSpaces int + + // Check that readSpaces does not read spaces when there are none + if fixture.runesBeforeSpaces > 0 { + err := readSpaces(reader, &actualNumSpaces) + require.NoError(err) + require.Equal(0, actualNumSpaces) + } + + // Read all the runes before the spaces + for i := 0; i < fixture.runesBeforeSpaces; i++ { + _, _, err := reader.ReadRune() + require.NoError(err) + } + + // Read all the spaces + err := readSpaces(reader, &actualNumSpaces) + require.NoError(err) + require.Equal(fixture.expectedNumSpaces, actualNumSpaces) + + actualRemaining, _ := reader.ReadString('\n') + require.Equal(fixture.expectedRemaining, actualRemaining) + } } diff --git a/sql/parse/views_test.go b/sql/parse/views_test.go new file mode 100644 index 0000000000..16b0e38b18 --- /dev/null +++ b/sql/parse/views_test.go @@ -0,0 +1,77 @@ +package parse + +import ( + "strings" + "testing" + + "github.com/src-d/go-mysql-server/sql" + "github.com/src-d/go-mysql-server/sql/expression" + "github.com/src-d/go-mysql-server/sql/plan" + "github.com/stretchr/testify/require" +) + +func TestParseCreateView(t *testing.T) { + var fixtures = map[string]sql.Node{ + `CREATE VIEW myview AS SELECT 1`: plan.NewCreateView( + sql.UnresolvedDatabase(""), + "myview", + nil, + plan.NewSubqueryAlias("myview", + plan.NewProject( + []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, + plan.NewUnresolvedTable("dual", ""), + ), + ), + false, + ), + `CREATE VIEW myview AS SELECT * FROM mytable`: plan.NewCreateView( + sql.UnresolvedDatabase(""), + "myview", + nil, + plan.NewSubqueryAlias("myview", + plan.NewProject( + []sql.Expression{expression.NewStar()}, + plan.NewUnresolvedTable("mytable", ""), + ), + ), + false, + ), + `CREATE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( + sql.UnresolvedDatabase("mydb"), + "myview", + nil, + plan.NewSubqueryAlias("myview", + plan.NewProject( + []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, + plan.NewUnresolvedTable("dual", ""), + ), + ), + false, + ), + `CREATE OR REPLACE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( + sql.UnresolvedDatabase("mydb"), + "myview", + nil, + plan.NewSubqueryAlias("myview", + plan.NewProject( + []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, + plan.NewUnresolvedTable("dual", ""), + ), + ), + true, + ), + } + + for query, expectedPlan := range fixtures { + t.Run(query, func(t *testing.T) { + require := require.New(t) + + ctx := sql.NewEmptyContext() + lowerquery := strings.ToLower(query) + result, err := parseCreateView(ctx, lowerquery) + + require.NoError(err) + require.Equal(expectedPlan, result) + }) + } +} diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 735a4bc7c0..9d27338bf6 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -7,6 +7,9 @@ import ( "github.com/src-d/go-mysql-server/sql" ) +// CreateView is a node representing the creation (or replacement) of a view, +// which is defined by the Child node. The Columns member represent the +// explicit columns specified by the query, if any. type CreateView struct { UnaryNode database sql.Database @@ -16,6 +19,8 @@ type CreateView struct { IsReplace bool } +// NewCreateView creates a CreateView node with the specified parameters, +// setting its catalog to nil. func NewCreateView( database sql.Database, name string, @@ -33,7 +38,7 @@ func NewCreateView( } } -// View returns the view that will be created by this node +// View returns the view that will be created by this node. func (create *CreateView) View() sql.View { return sql.NewView(create.Name, create.Child) } @@ -44,13 +49,17 @@ func (create *CreateView) Children() []sql.Node { return []sql.Node{create.Child} } -// Resolved implements the Node interface. +// Resolved implements the Node interface. This node is resolved if and only if +// the database and the Child are both resolved. func (create *CreateView) Resolved() bool { _, ok := create.database.(sql.UnresolvedDatabase) return !ok && create.Child.Resolved() } -// RowIter implements the Node interface. +// RowIter implements the Node interface. When executed, this function creates +// (or replaces) the view. It can error if the CraeteView's IsReplace member is +// set to false and the view already exists. The RowIter returned is always +// empty. func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { view := sql.NewView(create.Name, create.Child) @@ -61,9 +70,11 @@ func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { return sql.RowsToRowIter(), create.Catalog.ViewRegistry.Register(create.database.Name(), view) } -// Schema implements the Node interface. +// Schema implements the Node interface. It always returns nil. func (create *CreateView) Schema() sql.Schema { return nil } +// String implements the fmt.Stringer interface, using sql.TreePrinter to +// generate the string. func (create *CreateView) String() string { pr := sql.NewTreePrinter() _ = pr.WriteNode("CreateView(%s)", create.Name) @@ -74,7 +85,8 @@ func (create *CreateView) String() string { return pr.String() } -// WithChildren implements the Node interface. +// WithChildren implements the Node interface. It only succeeds if the length +// of the specified children equals 1. func (create *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { if len(children) != 1 { return nil, sql.ErrInvalidChildrenNumber.New(create, len(children), 1) @@ -85,10 +97,14 @@ func (create *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { return newCreate, nil } +// Database implements the Databaser interface, and it returns the database in +// which CreateView will create the view. func (create *CreateView) Database() sql.Database { return create.database } +// Database implements the Databaser interface, and it returns a copy of this +// node with the specified database. func (create *CreateView) WithDatabase(database sql.Database) (sql.Node, error) { newCreate := *create newCreate.database = database diff --git a/sql/plan/create_view_test.go b/sql/plan/create_view_test.go new file mode 100644 index 0000000000..d232f4d5f5 --- /dev/null +++ b/sql/plan/create_view_test.go @@ -0,0 +1,95 @@ +package plan + +import ( + "testing" + + "github.com/src-d/go-mysql-server/memory" + "github.com/src-d/go-mysql-server/sql" + "github.com/src-d/go-mysql-server/sql/expression" + + "github.com/stretchr/testify/require" +) + +func mockCreateView(isReplace bool) *CreateView { + table := memory.NewTable("mytable", sql.Schema{ + {Name: "i", Source: "mytable", Type: sql.Int32}, + {Name: "s", Source: "mytable", Type: sql.Text}, + }) + + db := memory.NewDatabase("db") + db.AddTable("db", table) + + catalog := sql.NewCatalog() + catalog.AddDatabase(db) + + subqueryAlias := NewSubqueryAlias("myview", + NewProject( + []sql.Expression{ + expression.NewGetFieldWithTable(1, sql.Int32, table.Name(), "i", true), + }, + NewUnresolvedTable("dual", ""), + ), + ) + + createView := NewCreateView(db, subqueryAlias.Name(), nil, subqueryAlias, isReplace) + createView.Catalog = catalog + + return createView +} + +// Tests that CreateView works as expected and that the view is registered in +// the catalog when RowIter is called +func TestCreateView(t *testing.T) { + require := require.New(t) + + createView := mockCreateView(false) + + ctx := sql.NewEmptyContext() + _, err := createView.RowIter(ctx) + require.NoError(err) + + expectedView := sql.NewView(createView.Name, createView.Child) + actualView, err := createView.Catalog.ViewRegistry.View(createView.database.Name(), createView.Name) + require.NoError(err) + require.Equal(expectedView, *actualView) +} + +// Tests that CreateView RowIter returns an error when the view exists +func TestCreateExistingView(t *testing.T) { + require := require.New(t) + + createView := mockCreateView(false) + + // Register a view with the same name + view := createView.View() + createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) + + ctx := sql.NewEmptyContext() + _, err := createView.RowIter(ctx) + require.Error(err) + require.True(sql.ErrExistingView.Is(err)) +} + +// Tests that CreateView RowIter succeeds when the view exists and the +// IsReplace flag is set to true +func TestReplaceExistingView(t *testing.T) { + require := require.New(t) + + createView := mockCreateView(true) + + // Register a view with the same name but no child + view := sql.NewView(createView.Name, nil) + createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) + + // Set the IsReplace flag to true + createView.IsReplace = true + + ctx := sql.NewEmptyContext() + _, err := createView.RowIter(ctx) + require.NoError(err) + + expectedView := createView.View() + actualView, err := createView.Catalog.ViewRegistry.View(createView.database.Name(), createView.Name) + require.NoError(err) + require.Equal(expectedView, *actualView) +} diff --git a/sql/viewregistry.go b/sql/viewregistry.go index 52e8945ab7..ec3616d3e6 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -12,49 +12,54 @@ var ( ErrNonExistingView = errors.NewKind("the view %s.%s does not exist in the registry") ) -// A View is defined by a Node and has a name +// A View is defined by a Node and has a name. type View struct { name string definition Node } -// Creates a View with the specified name and definition +// Creates a View with the specified name and definition. func NewView(name string, definition Node) View { return View{name, definition} } -// Returns the name of the view +// Returns the name of the view. func (view *View) Name() string { return view.name } -// Returns the definition of the view +// Returns the definition of the view. func (view *View) Definition() Node { return view.definition } // Views are scoped by the databases in which they were defined, so a key in -// the view registry is a pair of names: database and view +// the view registry is a pair of names: database and view. type viewKey struct { dbName, viewName string } -// Creates a viewKey ensuring both names are lowercase +// Creates a viewKey ensuring both names are lowercase. func newViewKey(databaseName, viewName string) viewKey { return viewKey{strings.ToLower(databaseName), strings.ToLower(viewName)} } +// ViewRegistry is a map of viewKey to View whose access is protected by a +// RWMutex. type ViewRegistry struct { mutex sync.RWMutex views map[viewKey]View } +// Creates an empty ViewRegistry. func NewViewRegistry() *ViewRegistry { return &ViewRegistry{ views: make(map[viewKey]View), } } +// Adds the view specified by the pair {database, view.Name()}, returning +// an error if there is already an element with that key. func (registry *ViewRegistry) Register(database string, view View) error { registry.mutex.Lock() defer registry.mutex.Unlock() @@ -70,7 +75,7 @@ func (registry *ViewRegistry) Register(database string, view View) error { } // Deletes the view specified by the pair {databaseName, viewName}, returning -// an error if it does not exist +// an error if it does not exist. func (registry *ViewRegistry) Delete(databaseName, viewName string) error { registry.mutex.Lock() defer registry.mutex.Unlock() @@ -85,6 +90,8 @@ func (registry *ViewRegistry) Delete(databaseName, viewName string) error { return nil } +// Returns a pointer to the view specified by the pair {databaseName, +// viewName}, returning an error if it does not exist. func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) { registry.mutex.RLock() defer registry.mutex.RUnlock() @@ -98,7 +105,7 @@ func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) return nil, ErrNonExistingView.New(databaseName, viewName) } -// Returns the map of all views in the registry +// Returns the map of all views in the registry. func (registry *ViewRegistry) AllViews() map[viewKey]View { registry.mutex.RLock() defer registry.mutex.RUnlock() @@ -106,7 +113,7 @@ func (registry *ViewRegistry) AllViews() map[viewKey]View { return registry.views } -// Returns an array of all the views registered under the specified database +// Returns an array of all the views registered under the specified database. func (registry *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { registry.mutex.RLock() defer registry.mutex.RUnlock() diff --git a/sql/viewregistry_test.go b/sql/viewregistry_test.go new file mode 100644 index 0000000000..de31246bdd --- /dev/null +++ b/sql/viewregistry_test.go @@ -0,0 +1,134 @@ +package sql + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + dbName = "db" + viewName = "myview" + mockView = NewView(viewName, nil) +) + +// Tests the creation of an empty ViewRegistry with no views registered. +func TestNewViewRegistry(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + require.Equal(0, len(registry.AllViews())) +} + +// Tests that registering a non-existing view succeeds. +func TestRegisterNonExistingView(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + err := registry.Register(dbName, mockView) + require.NoError(err) + require.Equal(1, len(registry.AllViews())) + + actualView, err := registry.View(dbName, viewName) + require.NoError(err) + require.Equal(mockView, *actualView) +} + +// Tests that registering an existing view fails. +func TestRegisterExistingVIew(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + err := registry.Register(dbName, mockView) + require.NoError(err) + require.Equal(1, len(registry.AllViews())) + + // Try to register the same view once again + err = registry.Register(dbName, mockView) + require.Error(err) + require.True(ErrExistingView.Is(err)) +} + +// Tests that deleting an existing view succeeds. +func TestDeleteExistingView(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + err := registry.Register(dbName, mockView) + require.NoError(err) + require.Equal(1, len(registry.AllViews())) + + err = registry.Delete(dbName, viewName) + require.NoError(err) + require.Equal(0, len(registry.AllViews())) +} + +// Tests that deleting a non-existing view fails. +func TestDeleteNonExistingView(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + err := registry.Delete("random", "randomer") + require.Error(err) + require.True(ErrNonExistingView.Is(err)) +} + +// Tests that retrieving an existing view succeeds and that the view returned +// is the correct one. +func TestGetExistingView(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + err := registry.Register(dbName, mockView) + require.NoError(err) + require.Equal(1, len(registry.AllViews())) + + actualView, err := registry.View(dbName, viewName) + require.NoError(err) + require.Equal(mockView, *actualView) +} + +// Tests that retrieving a non-existing view fails. +func TestGetNonExistingView(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + actualView, err := registry.View(dbName, viewName) + require.Error(err) + require.Nil(actualView) + require.True(ErrNonExistingView.Is(err)) +} + +// Tests that retrieving the views registered under a database succeeds, +// returning the list of all the correct views. +func TestViewsInDatabase(t *testing.T) { + require := require.New(t) + + registry := NewViewRegistry() + + databases := []struct { + name string + numViews int + }{ + {"db0", 0}, + {"db1", 5}, + {"db2", 10}, + } + + for _, db := range databases { + for i := 0; i < db.numViews; i++ { + view := NewView(viewName+string(i), nil) + err := registry.Register(db.name, view) + require.NoError(err) + } + + views := registry.ViewsInDatabase(db.name) + require.Equal(db.numViews, len(views)) + } +} From 9108273765ddda0c5f765ede6da7cc2aed9deb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 12:37:21 +0200 Subject: [PATCH 13/33] Fix error returning in parsing function maybeList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index b3464a9881..b5a13a21c4 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -483,8 +483,7 @@ func maybeList(opening, separator, closing rune, list *[]string) parseFunc { } if r != opening { - rd.UnreadRune() - return nil + return rd.UnreadRune() } for { From 202d256b73e604eef2ac9562be4699986c53f129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 12:38:35 +0200 Subject: [PATCH 14/33] Make sure all errors are checked in VIEW-related tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trying to make golangcibot happy Signed-off-by: Alejandro García Montoro --- sql/analyzer/resolve_tables_test.go | 3 ++- sql/parse/util_test.go | 18 ++++++++++++++++-- sql/plan/create_view_test.go | 10 ++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/sql/analyzer/resolve_tables_test.go b/sql/analyzer/resolve_tables_test.go index 1d00d787a6..b2decfbffc 100644 --- a/sql/analyzer/resolve_tables_test.go +++ b/sql/analyzer/resolve_tables_test.go @@ -118,7 +118,8 @@ func TestResolveViews(t *testing.T) { // Register the view in the catalog catalog := sql.NewCatalog() catalog.AddDatabase(db) - catalog.ViewRegistry.Register(db.Name(), view) + err := catalog.ViewRegistry.Register(db.Name(), view) + require.NoError(err) a := NewBuilder(catalog).AddPostAnalyzeRule(f.Name, f.Apply).Build() diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go index b5675f56e6..2d187217af 100644 --- a/sql/parse/util_test.go +++ b/sql/parse/util_test.go @@ -61,7 +61,8 @@ func TestReadLetterOrPoint(t *testing.T) { var buffer bytes.Buffer for i := 0; i < len(fixture.string); i++ { - readLetterOrPoint(reader, &buffer) + err := readLetterOrPoint(reader, &buffer) + require.NoError(err) } remaining, _ := reader.ReadString('\n') @@ -82,42 +83,49 @@ func TestReadValidScopedIdentRune(t *testing.T) { separator rune expectedBuffer string expectedRemaining string + expectedError bool }{ { "ident_1.ident_2", '.', "ident_1.ident_2", "", + false, }, { "$ident_1.ident_2", '.', "", "$ident_1.ident_2", + true, }, { "", '.', "", "", + false, }, { "ident_1 ident_2", '.', "ident_1", " ident_2", + true, }, { "ident_1 ident_2", ' ', "ident_1 ident_2", "", + false, }, { "ident_1.ident_2 ident_3", '.', "ident_1.ident_2", " ident_3", + true, }, } @@ -125,8 +133,14 @@ func TestReadValidScopedIdentRune(t *testing.T) { reader := bufio.NewReader(strings.NewReader(fixture.string)) var buffer bytes.Buffer + var err error for i := 0; i < len(fixture.string); i++ { - readValidScopedIdentRune(reader, fixture.separator, &buffer) + err = readValidScopedIdentRune(reader, fixture.separator, &buffer) + } + if fixture.expectedError { + require.Error(err) + } else { + require.NoError(err) } remaining, _ := reader.ReadString('\n') diff --git a/sql/plan/create_view_test.go b/sql/plan/create_view_test.go index d232f4d5f5..3b768fda10 100644 --- a/sql/plan/create_view_test.go +++ b/sql/plan/create_view_test.go @@ -62,10 +62,11 @@ func TestCreateExistingView(t *testing.T) { // Register a view with the same name view := createView.View() - createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) + err := createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) + require.NoError(err) ctx := sql.NewEmptyContext() - _, err := createView.RowIter(ctx) + _, err = createView.RowIter(ctx) require.Error(err) require.True(sql.ErrExistingView.Is(err)) } @@ -79,13 +80,14 @@ func TestReplaceExistingView(t *testing.T) { // Register a view with the same name but no child view := sql.NewView(createView.Name, nil) - createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) + err := createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) + require.NoError(err) // Set the IsReplace flag to true createView.IsReplace = true ctx := sql.NewEmptyContext() - _, err := createView.RowIter(ctx) + _, err = createView.RowIter(ctx) require.NoError(err) expectedView := createView.View() From d27b3d31d451d9ac8719d8c9617f2a40ad2d944d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 14:47:14 +0200 Subject: [PATCH 15/33] Rename readScopedIdent to readIdentList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 6 +++--- sql/parse/util_test.go | 6 +++--- sql/parse/views.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index b5a13a21c4..8f8f07144e 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -262,14 +262,14 @@ func readIdent(ident *string) parseFunc { } } -// Reads a scoped identifier, populating the specified slice with the different -// parts of the identifier if it is correctly formed. +// readIdentList reads a scoped identifier, populating the specified slice +// with the different parts of the identifier if it is correctly formed. // A scoped identifier is a sequence of identifiers separated by the specified // rune in separator. An identifier is a string of runes whose first character // is a letter and the following ones are either letters, digits or underscores. // An example of a correctly formed scoped identifier is "dbName.tableName", // that would populate the slice with the values ["dbName", "tableName"] -func readScopedIdent(separator rune, idents *[]string) parseFunc { +func readIdentList(separator rune, idents *[]string) parseFunc { return func(r *bufio.Reader) error { var buf bytes.Buffer if err := readLetter(r, &buf); err != nil { diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go index 2d187217af..ee8395f8fc 100644 --- a/sql/parse/util_test.go +++ b/sql/parse/util_test.go @@ -150,9 +150,9 @@ func TestReadValidScopedIdentRune(t *testing.T) { } } -// Tests that readScopedIdent reads a list of identifiers separated by a user- +// Tests that readIdentList reads a list of identifiers separated by a user- // specified rune, populating the passed slice with the identifiers found. -func TestReadScopedIdent(t *testing.T) { +func TestReadIdentList(t *testing.T) { require := require.New(t) testFixtures := []struct { @@ -203,7 +203,7 @@ func TestReadScopedIdent(t *testing.T) { reader := bufio.NewReader(strings.NewReader(fixture.string)) var actualIdents []string - err := readScopedIdent(fixture.separator, &actualIdents)(reader) + err := readIdentList(fixture.separator, &actualIdents)(reader) require.NoError(err) remaining, _ := reader.ReadString('\n') diff --git a/sql/parse/views.go b/sql/parse/views.go index 30e9d3e5e2..9b41e81b41 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -35,7 +35,7 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { skipSpaces, expect("view"), skipSpaces, - readScopedIdent('.', &scopedName), + readIdentList('.', &scopedName), skipSpaces, maybeList('(', ',', ')', &columns), skipSpaces, From af5965283dbd608cf5731812afe54e5f687b1620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 14:47:45 +0200 Subject: [PATCH 16/33] Modify VIEW-related comments to adhere to Go conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/resolve_tables_test.go | 4 ---- sql/parse/util.go | 28 +++++++++++++++------------- sql/parse/views.go | 2 +- sql/plan/create_view_test.go | 3 --- sql/viewregistry.go | 27 ++++++++++++++------------- sql/viewregistry_test.go | 1 - 6 files changed, 30 insertions(+), 35 deletions(-) diff --git a/sql/analyzer/resolve_tables_test.go b/sql/analyzer/resolve_tables_test.go index b2decfbffc..fe22ab2ede 100644 --- a/sql/analyzer/resolve_tables_test.go +++ b/sql/analyzer/resolve_tables_test.go @@ -115,7 +115,6 @@ func TestResolveViews(t *testing.T) { subqueryAlias := plan.NewSubqueryAlias("myview", subquery) view := sql.NewView("myview", subqueryAlias) - // Register the view in the catalog catalog := sql.NewCatalog() catalog.AddDatabase(db) err := catalog.ViewRegistry.Register(db.Name(), view) @@ -123,19 +122,16 @@ func TestResolveViews(t *testing.T) { a := NewBuilder(catalog).AddPostAnalyzeRule(f.Name, f.Apply).Build() - // Check whether the view is resolved and replaced with the subquery var notAnalyzed sql.Node = plan.NewUnresolvedTable("myview", "") analyzed, err := f.Apply(sql.NewEmptyContext(), a, notAnalyzed) require.NoError(err) require.Equal(subqueryAlias, analyzed) - // Ensures that the resolution is case-insensitive notAnalyzed = plan.NewUnresolvedTable("MyVieW", "") analyzed, err = f.Apply(sql.NewEmptyContext(), a, notAnalyzed) require.NoError(err) require.Equal(subqueryAlias, analyzed) - // Ensures that the resolution is idempotent analyzed, err = f.Apply(sql.NewEmptyContext(), a, subqueryAlias) require.NoError(err) require.Equal(subqueryAlias, analyzed) diff --git a/sql/parse/util.go b/sql/parse/util.go index 8f8f07144e..0143868560 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -80,8 +80,8 @@ func skipSpaces(r *bufio.Reader) error { } } -// Reads every contiguous space from the reader, populating numSpacesRead with -// the number of spaces read. +// readSpaces reads every contiguous space from the reader, populating +// numSpacesRead with the number of spaces read. func readSpaces(r *bufio.Reader, numSpacesRead *int) error { *numSpacesRead = 0 for { @@ -148,8 +148,8 @@ func readLetter(r *bufio.Reader, buf *bytes.Buffer) error { return nil } -// Parses a single rune from the reader and consumes it, copying it to the -// buffer, if it is either a letter or a point +// readLetterOrPoint parses a single rune from the reader and consumes it, +// copying it to the buffer, if it is either a letter or a point func readLetterOrPoint(r *bufio.Reader, buf *bytes.Buffer) error { ru, _, err := r.ReadRune() if err != nil { @@ -188,8 +188,9 @@ func readValidIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { return nil } -// Parses a single rune from the reader and consumes it, copying it to the -// buffer, if is a letter, a digit, an underscore or the specified separator. +// readValidScopedIdentRune parses a single rune from the reader and consumes +// it, copying it to the buffer, if is a letter, a digit, an underscore or the +// specified separator. func readValidScopedIdentRune(r *bufio.Reader, separator rune, buf *bytes.Buffer) error { ru, _, err := r.ReadRune() if err != nil { @@ -404,8 +405,8 @@ func expectQuote(r *bufio.Reader) error { return nil } -// Tries to read the specified string, consuming the reader if the string is -// found. The `matched` boolean is set to true if the string is found +// maybe tries to read the specified string, consuming the reader if the string +// is found. The `matched` boolean is set to true if the string is found func maybe(matched *bool, str string) parseFunc { return func(rd *bufio.Reader) error { *matched = false @@ -436,9 +437,9 @@ func maybe(matched *bool, str string) parseFunc { } } -// Tries to read the specified strings, one after the other, separated by an -// arbitrary number of spaces. It consumes the reader if and only if all the -// strings are found. +// multiMaybe tries to read the specified strings, one after the other, +// separated by an arbitrary number of spaces. It consumes the reader if and +// only if all the strings are found. func multiMaybe(matched *bool, strings ...string) parseFunc { return func(rd *bufio.Reader) error { *matched = false @@ -468,8 +469,9 @@ func multiMaybe(matched *bool, strings ...string) parseFunc { } } -// Reads a list of strings separated by the specified separator, with a rune -// indicating the opening of the list and another one specifying its closing. +// maybeList reads a list of strings separated by the specified separator, with +// a rune indicating the opening of the list and another one specifying its +// closing. // For example, readList('(', ',', ')', list) parses "(uno, dos,tres)" and // populates list with the array of strings ["uno", "dos", "tres"] // If the opening is not found, this does not consumes any rune from the diff --git a/sql/parse/views.go b/sql/parse/views.go index 9b41e81b41..11b7bbf2fb 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -14,7 +14,7 @@ import ( var ErrMalformedViewName = errors.NewKind("the view name '%s' is not correct") var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT query") -// Parses +// parseCreateView parses // CREATE [OR REPLACE] VIEW [db_name.]view_name AS select_statement // and returns a NewCreateView node in case of success func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { diff --git a/sql/plan/create_view_test.go b/sql/plan/create_view_test.go index 3b768fda10..d9b4444a0f 100644 --- a/sql/plan/create_view_test.go +++ b/sql/plan/create_view_test.go @@ -60,7 +60,6 @@ func TestCreateExistingView(t *testing.T) { createView := mockCreateView(false) - // Register a view with the same name view := createView.View() err := createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) require.NoError(err) @@ -78,12 +77,10 @@ func TestReplaceExistingView(t *testing.T) { createView := mockCreateView(true) - // Register a view with the same name but no child view := sql.NewView(createView.Name, nil) err := createView.Catalog.ViewRegistry.Register(createView.database.Name(), view) require.NoError(err) - // Set the IsReplace flag to true createView.IsReplace = true ctx := sql.NewEmptyContext() diff --git a/sql/viewregistry.go b/sql/viewregistry.go index ec3616d3e6..7b5797ddb6 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -12,23 +12,23 @@ var ( ErrNonExistingView = errors.NewKind("the view %s.%s does not exist in the registry") ) -// A View is defined by a Node and has a name. +// View is defined by a Node and has a name. type View struct { name string definition Node } -// Creates a View with the specified name and definition. +// NewView creates a View with the specified name and definition. func NewView(name string, definition Node) View { return View{name, definition} } -// Returns the name of the view. +// Name returns the name of the view. func (view *View) Name() string { return view.name } -// Returns the definition of the view. +// Definition returns the definition of the view. func (view *View) Definition() Node { return view.definition } @@ -39,7 +39,7 @@ type viewKey struct { dbName, viewName string } -// Creates a viewKey ensuring both names are lowercase. +// newViewKey creates a viewKey ensuring both names are lowercase. func newViewKey(databaseName, viewName string) viewKey { return viewKey{strings.ToLower(databaseName), strings.ToLower(viewName)} } @@ -51,15 +51,15 @@ type ViewRegistry struct { views map[viewKey]View } -// Creates an empty ViewRegistry. +// NewViewRegistry creates an empty ViewRegistry. func NewViewRegistry() *ViewRegistry { return &ViewRegistry{ views: make(map[viewKey]View), } } -// Adds the view specified by the pair {database, view.Name()}, returning -// an error if there is already an element with that key. +// Register adds the view specified by the pair {database, view.Name()}, +// returning an error if there is already an element with that key. func (registry *ViewRegistry) Register(database string, view View) error { registry.mutex.Lock() defer registry.mutex.Unlock() @@ -74,8 +74,8 @@ func (registry *ViewRegistry) Register(database string, view View) error { return nil } -// Deletes the view specified by the pair {databaseName, viewName}, returning -// an error if it does not exist. +// Delete deletes the view specified by the pair {databaseName, viewName}, +// returning an error if it does not exist. func (registry *ViewRegistry) Delete(databaseName, viewName string) error { registry.mutex.Lock() defer registry.mutex.Unlock() @@ -90,7 +90,7 @@ func (registry *ViewRegistry) Delete(databaseName, viewName string) error { return nil } -// Returns a pointer to the view specified by the pair {databaseName, +// View returns a pointer to the view specified by the pair {databaseName, // viewName}, returning an error if it does not exist. func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) { registry.mutex.RLock() @@ -105,7 +105,7 @@ func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) return nil, ErrNonExistingView.New(databaseName, viewName) } -// Returns the map of all views in the registry. +// AllViews returns the map of all views in the registry. func (registry *ViewRegistry) AllViews() map[viewKey]View { registry.mutex.RLock() defer registry.mutex.RUnlock() @@ -113,7 +113,8 @@ func (registry *ViewRegistry) AllViews() map[viewKey]View { return registry.views } -// Returns an array of all the views registered under the specified database. +// ViewsInDatabase returns an array of all the views registered under the +// specified database. func (registry *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { registry.mutex.RLock() defer registry.mutex.RUnlock() diff --git a/sql/viewregistry_test.go b/sql/viewregistry_test.go index de31246bdd..f33a88c606 100644 --- a/sql/viewregistry_test.go +++ b/sql/viewregistry_test.go @@ -45,7 +45,6 @@ func TestRegisterExistingVIew(t *testing.T) { require.NoError(err) require.Equal(1, len(registry.AllViews())) - // Try to register the same view once again err = registry.Register(dbName, mockView) require.Error(err) require.True(ErrExistingView.Is(err)) From 16413c942a0ee2c87dcdfc08a457b698bf0d1606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 14:53:25 +0200 Subject: [PATCH 17/33] Refactor skipSpaces so it calls readSpaces discarding the count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index 0143868560..ed60a09e41 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -64,20 +64,8 @@ func expect(expected string) parseFunc { } func skipSpaces(r *bufio.Reader) error { - for { - ru, _, err := r.ReadRune() - if err == io.EOF { - return nil - } - - if err != nil { - return err - } - - if !unicode.IsSpace(ru) { - return r.UnreadRune() - } - } + var unusedCount int + return readSpaces(r, &unusedCount) } // readSpaces reads every contiguous space from the reader, populating From 05a24563c48f8901207a8cecc5f444d3a4ef5ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 15:22:24 +0200 Subject: [PATCH 18/33] Improve the efficiency of readIdentList (HT @erizocosmico) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before, this function read the string containing all the identifiers *and* the separators between them, splitting the string afterwards to retrieve the list. Now, the list of identifiers is built on the go. Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 33 ++++++++++++++++++++------------- sql/parse/util_test.go | 9 +++++++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index ed60a09e41..b438ad3493 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -179,21 +179,22 @@ func readValidIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { // readValidScopedIdentRune parses a single rune from the reader and consumes // it, copying it to the buffer, if is a letter, a digit, an underscore or the // specified separator. -func readValidScopedIdentRune(r *bufio.Reader, separator rune, buf *bytes.Buffer) error { +// If the returned error is not nil, the returned rune equals the null +// character. +func readValidScopedIdentRune(r *bufio.Reader, separator rune) (rune, error) { ru, _, err := r.ReadRune() if err != nil { - return err + return 0, err } if !unicode.IsLetter(ru) && !unicode.IsDigit(ru) && ru != '_' && ru != separator { if err := r.UnreadRune(); err != nil { - return err + return 0, err } - return io.EOF + return 0, io.EOF } - buf.WriteRune(ru) - return nil + return ru, nil } func readValidQuotedIdentRune(r *bufio.Reader, buf *bytes.Buffer) error { @@ -266,18 +267,24 @@ func readIdentList(separator rune, idents *[]string) parseFunc { } for { - if err := readValidScopedIdentRune(r, separator, &buf); err == io.EOF { - break - } else if err != nil { + currentRune, err := readValidScopedIdentRune(r, separator) + if err != nil { + if err == io.EOF { + break + } return err } + + if currentRune == separator { + *idents = append(*idents, buf.String()) + buf.Reset() + } else { + buf.WriteRune(currentRune) + } } if readString := buf.String(); len(readString) > 0 { - *idents = append( - *idents, - strings.Split(strings.ToLower(readString), string(separator))..., - ) + *idents = append(*idents, readString) } return nil } diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go index ee8395f8fc..b1cd07fe59 100644 --- a/sql/parse/util_test.go +++ b/sql/parse/util_test.go @@ -133,9 +133,14 @@ func TestReadValidScopedIdentRune(t *testing.T) { reader := bufio.NewReader(strings.NewReader(fixture.string)) var buffer bytes.Buffer + var rune rune var err error for i := 0; i < len(fixture.string); i++ { - err = readValidScopedIdentRune(reader, fixture.separator, &buffer) + if rune, err = readValidScopedIdentRune(reader, fixture.separator); err != nil { + break + } + + buffer.WriteRune(rune) } if fixture.expectedError { require.Error(err) @@ -146,7 +151,7 @@ func TestReadValidScopedIdentRune(t *testing.T) { remaining, _ := reader.ReadString('\n') require.Equal(remaining, fixture.expectedRemaining) - require.Equal(buffer.String(), fixture.expectedBuffer) + require.Equal(fixture.expectedBuffer, buffer.String()) } } From 3fee47c11c1386f3dca10f85e80a7a9ab3e62efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 15:30:29 +0200 Subject: [PATCH 19/33] Improve error management in CreateView node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can discard the error if it was thrown because the view does not exist. But we don't know if the Delete function will throw different errors in the future, so it is safer to discard only the one we are sure about. Signed-off-by: Alejandro García Montoro --- sql/plan/create_view.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 9d27338bf6..65da4cce73 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -62,12 +62,16 @@ func (create *CreateView) Resolved() bool { // empty. func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { view := sql.NewView(create.Name, create.Child) + registry := create.Catalog.ViewRegistry if create.IsReplace { - _ = create.Catalog.ViewRegistry.Delete(create.database.Name(), view.Name()) + err := registry.Delete(create.database.Name(), view.Name()) + if err != nil && !sql.ErrNonExistingView.Is(err) { + return sql.RowsToRowIter(), err + } } - return sql.RowsToRowIter(), create.Catalog.ViewRegistry.Register(create.database.Name(), view) + return sql.RowsToRowIter(), registry.Register(create.database.Name(), view) } // Schema implements the Node interface. It always returns nil. From 660fb4fa7d3df2d78fa5d666cfbc0bba2eeac6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 15:45:19 +0200 Subject: [PATCH 20/33] Remove redundant comment in test function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/resolve_tables_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/sql/analyzer/resolve_tables_test.go b/sql/analyzer/resolve_tables_test.go index fe22ab2ede..d2fc34fedb 100644 --- a/sql/analyzer/resolve_tables_test.go +++ b/sql/analyzer/resolve_tables_test.go @@ -92,9 +92,6 @@ func TestResolveTablesNested(t *testing.T) { require.Equal(expected, analyzed) } -// Tests the resolution of views (ensuring it is case-insensitive), that should -// result in the replacement of the UnresolvedTable with the SubqueryAlias that -// represents the view func TestResolveViews(t *testing.T) { require := require.New(t) From 685c869a3f496f49b7ae49a1c4847e57f673364d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 25 Oct 2019 15:59:59 +0200 Subject: [PATCH 21/33] Shorten receiver names, adhering to Go conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/plan/create_view.go | 50 +++++++++++++++++++-------------------- sql/viewregistry.go | 52 ++++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/sql/plan/create_view.go b/sql/plan/create_view.go index 65da4cce73..6d7e7c8bde 100644 --- a/sql/plan/create_view.go +++ b/sql/plan/create_view.go @@ -39,78 +39,78 @@ func NewCreateView( } // View returns the view that will be created by this node. -func (create *CreateView) View() sql.View { - return sql.NewView(create.Name, create.Child) +func (cv *CreateView) View() sql.View { + return sql.NewView(cv.Name, cv.Child) } // Children implements the Node interface. It returns the Child of the // CreateView node; i.e., the definition of the view that will be created. -func (create *CreateView) Children() []sql.Node { - return []sql.Node{create.Child} +func (cv *CreateView) Children() []sql.Node { + return []sql.Node{cv.Child} } // Resolved implements the Node interface. This node is resolved if and only if // the database and the Child are both resolved. -func (create *CreateView) Resolved() bool { - _, ok := create.database.(sql.UnresolvedDatabase) - return !ok && create.Child.Resolved() +func (cv *CreateView) Resolved() bool { + _, ok := cv.database.(sql.UnresolvedDatabase) + return !ok && cv.Child.Resolved() } // RowIter implements the Node interface. When executed, this function creates // (or replaces) the view. It can error if the CraeteView's IsReplace member is // set to false and the view already exists. The RowIter returned is always // empty. -func (create *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { - view := sql.NewView(create.Name, create.Child) - registry := create.Catalog.ViewRegistry +func (cv *CreateView) RowIter(ctx *sql.Context) (sql.RowIter, error) { + view := sql.NewView(cv.Name, cv.Child) + registry := cv.Catalog.ViewRegistry - if create.IsReplace { - err := registry.Delete(create.database.Name(), view.Name()) + if cv.IsReplace { + err := registry.Delete(cv.database.Name(), view.Name()) if err != nil && !sql.ErrNonExistingView.Is(err) { return sql.RowsToRowIter(), err } } - return sql.RowsToRowIter(), registry.Register(create.database.Name(), view) + return sql.RowsToRowIter(), registry.Register(cv.database.Name(), view) } // Schema implements the Node interface. It always returns nil. -func (create *CreateView) Schema() sql.Schema { return nil } +func (cv *CreateView) Schema() sql.Schema { return nil } // String implements the fmt.Stringer interface, using sql.TreePrinter to // generate the string. -func (create *CreateView) String() string { +func (cv *CreateView) String() string { pr := sql.NewTreePrinter() - _ = pr.WriteNode("CreateView(%s)", create.Name) + _ = pr.WriteNode("CreateView(%s)", cv.Name) _ = pr.WriteChildren( - fmt.Sprintf("Columns (%s)", strings.Join(create.Columns, ", ")), - create.Child.String(), + fmt.Sprintf("Columns (%s)", strings.Join(cv.Columns, ", ")), + cv.Child.String(), ) return pr.String() } // WithChildren implements the Node interface. It only succeeds if the length // of the specified children equals 1. -func (create *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { +func (cv *CreateView) WithChildren(children ...sql.Node) (sql.Node, error) { if len(children) != 1 { - return nil, sql.ErrInvalidChildrenNumber.New(create, len(children), 1) + return nil, sql.ErrInvalidChildrenNumber.New(cv, len(children), 1) } - newCreate := create + newCreate := cv newCreate.Child = children[0] return newCreate, nil } // Database implements the Databaser interface, and it returns the database in // which CreateView will create the view. -func (create *CreateView) Database() sql.Database { - return create.database +func (cv *CreateView) Database() sql.Database { + return cv.database } // Database implements the Databaser interface, and it returns a copy of this // node with the specified database. -func (create *CreateView) WithDatabase(database sql.Database) (sql.Node, error) { - newCreate := *create +func (cv *CreateView) WithDatabase(database sql.Database) (sql.Node, error) { + newCreate := *cv newCreate.database = database return &newCreate, nil } diff --git a/sql/viewregistry.go b/sql/viewregistry.go index 7b5797ddb6..bf62f13970 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -24,13 +24,13 @@ func NewView(name string, definition Node) View { } // Name returns the name of the view. -func (view *View) Name() string { - return view.name +func (v *View) Name() string { + return v.name } // Definition returns the definition of the view. -func (view *View) Definition() Node { - return view.definition +func (v *View) Definition() Node { + return v.definition } // Views are scoped by the databases in which they were defined, so a key in @@ -60,45 +60,45 @@ func NewViewRegistry() *ViewRegistry { // Register adds the view specified by the pair {database, view.Name()}, // returning an error if there is already an element with that key. -func (registry *ViewRegistry) Register(database string, view View) error { - registry.mutex.Lock() - defer registry.mutex.Unlock() +func (r *ViewRegistry) Register(database string, view View) error { + r.mutex.Lock() + defer r.mutex.Unlock() key := newViewKey(database, view.Name()) - if _, ok := registry.views[key]; ok { + if _, ok := r.views[key]; ok { return ErrExistingView.New(database, view.Name()) } - registry.views[key] = view + r.views[key] = view return nil } // Delete deletes the view specified by the pair {databaseName, viewName}, // returning an error if it does not exist. -func (registry *ViewRegistry) Delete(databaseName, viewName string) error { - registry.mutex.Lock() - defer registry.mutex.Unlock() +func (r *ViewRegistry) Delete(databaseName, viewName string) error { + r.mutex.Lock() + defer r.mutex.Unlock() key := newViewKey(databaseName, viewName) - if _, ok := registry.views[key]; !ok { + if _, ok := r.views[key]; !ok { return ErrNonExistingView.New(databaseName, viewName) } - delete(registry.views, key) + delete(r.views, key) return nil } // View returns a pointer to the view specified by the pair {databaseName, // viewName}, returning an error if it does not exist. -func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) { - registry.mutex.RLock() - defer registry.mutex.RUnlock() +func (r *ViewRegistry) View(databaseName, viewName string) (*View, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() key := newViewKey(databaseName, viewName) - if view, ok := registry.views[key]; ok { + if view, ok := r.views[key]; ok { return &view, nil } @@ -106,20 +106,20 @@ func (registry *ViewRegistry) View(databaseName, viewName string) (*View, error) } // AllViews returns the map of all views in the registry. -func (registry *ViewRegistry) AllViews() map[viewKey]View { - registry.mutex.RLock() - defer registry.mutex.RUnlock() +func (r *ViewRegistry) AllViews() map[viewKey]View { + r.mutex.RLock() + defer r.mutex.RUnlock() - return registry.views + return r.views } // ViewsInDatabase returns an array of all the views registered under the // specified database. -func (registry *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { - registry.mutex.RLock() - defer registry.mutex.RUnlock() +func (r *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { + r.mutex.RLock() + defer r.mutex.RUnlock() - for key, value := range registry.views { + for key, value := range r.views { if key.dbName == databaseName { views = append(views, value) } From a006ec79703a4213e305497dd770819744280de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Mon, 28 Oct 2019 08:58:02 +0100 Subject: [PATCH 22/33] Check permissions on CREATE VIEW statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- engine.go | 2 +- engine_test.go | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/engine.go b/engine.go index 6be2be3c2f..a622f2b24b 100644 --- a/engine.go +++ b/engine.go @@ -120,7 +120,7 @@ func (e *Engine) Query( case *plan.CreateIndex: typ = sql.CreateIndexProcess perm = auth.ReadPerm | auth.WritePerm - case *plan.InsertInto, *plan.DeleteFrom, *plan.Update, *plan.DropIndex, *plan.UnlockTables, *plan.LockTables: + case *plan.InsertInto, *plan.DeleteFrom, *plan.Update, *plan.DropIndex, *plan.UnlockTables, *plan.LockTables, *plan.CreateView: perm = auth.ReadPerm | auth.WritePerm } diff --git a/engine_test.go b/engine_test.go index b022a3e6ba..8846d0ebdd 100644 --- a/engine_test.go +++ b/engine_test.go @@ -3144,17 +3144,18 @@ func TestReadOnly(t *testing.T) { _, _, err := e.Query(newCtx(), `SELECT i FROM mytable`) require.NoError(err) - _, _, err = e.Query(newCtx(), `CREATE INDEX foo ON mytable USING pilosa (i, s)`) - require.Error(err) - require.True(auth.ErrNotAuthorized.Is(err)) - - _, _, err = e.Query(newCtx(), `DROP INDEX foo ON mytable`) - require.Error(err) - require.True(auth.ErrNotAuthorized.Is(err)) + writingQueries := []string{ + `CREATE INDEX foo ON mytable USING pilosa (i, s)`, + `DROP INDEX foo ON mytable`, + `INSERT INTO mytable (i, s) VALUES(42, 'yolo')`, + `CREATE VIEW myview AS SELECT i FROM mytable`, + } - _, _, err = e.Query(newCtx(), `INSERT INTO mytable (i, s) VALUES(42, 'yolo')`) - require.Error(err) - require.True(auth.ErrNotAuthorized.Is(err)) + for _, query := range writingQueries { + _, _, err = e.Query(newCtx(), query) + require.Error(err) + require.True(auth.ErrNotAuthorized.Is(err)) + } } func TestSessionVariables(t *testing.T) { From 02c010c6353262634f153c875194cf1c2cd9d13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Mon, 28 Oct 2019 16:17:27 +0100 Subject: [PATCH 23/33] Make ViewKey struct public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/viewregistry.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sql/viewregistry.go b/sql/viewregistry.go index bf62f13970..77dda35066 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -35,26 +35,26 @@ func (v *View) Definition() Node { // Views are scoped by the databases in which they were defined, so a key in // the view registry is a pair of names: database and view. -type viewKey struct { +type ViewKey struct { dbName, viewName string } -// newViewKey creates a viewKey ensuring both names are lowercase. -func newViewKey(databaseName, viewName string) viewKey { - return viewKey{strings.ToLower(databaseName), strings.ToLower(viewName)} +// NewViewKey creates a ViewKey ensuring both names are lowercase. +func NewViewKey(databaseName, viewName string) ViewKey { + return ViewKey{strings.ToLower(databaseName), strings.ToLower(viewName)} } -// ViewRegistry is a map of viewKey to View whose access is protected by a +// ViewRegistry is a map of ViewKey to View whose access is protected by a // RWMutex. type ViewRegistry struct { mutex sync.RWMutex - views map[viewKey]View + views map[ViewKey]View } // NewViewRegistry creates an empty ViewRegistry. func NewViewRegistry() *ViewRegistry { return &ViewRegistry{ - views: make(map[viewKey]View), + views: make(map[ViewKey]View), } } @@ -64,7 +64,7 @@ func (r *ViewRegistry) Register(database string, view View) error { r.mutex.Lock() defer r.mutex.Unlock() - key := newViewKey(database, view.Name()) + key := NewViewKey(database, view.Name()) if _, ok := r.views[key]; ok { return ErrExistingView.New(database, view.Name()) @@ -80,7 +80,7 @@ func (r *ViewRegistry) Delete(databaseName, viewName string) error { r.mutex.Lock() defer r.mutex.Unlock() - key := newViewKey(databaseName, viewName) + key := NewViewKey(databaseName, viewName) if _, ok := r.views[key]; !ok { return ErrNonExistingView.New(databaseName, viewName) @@ -96,7 +96,7 @@ func (r *ViewRegistry) View(databaseName, viewName string) (*View, error) { r.mutex.RLock() defer r.mutex.RUnlock() - key := newViewKey(databaseName, viewName) + key := NewViewKey(databaseName, viewName) if view, ok := r.views[key]; ok { return &view, nil @@ -106,7 +106,7 @@ func (r *ViewRegistry) View(databaseName, viewName string) (*View, error) { } // AllViews returns the map of all views in the registry. -func (r *ViewRegistry) AllViews() map[viewKey]View { +func (r *ViewRegistry) AllViews() map[ViewKey]View { r.mutex.RLock() defer r.mutex.RUnlock() From fc974142bcc2f89f8fab1b24b5615b2ca05ecf1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Mon, 28 Oct 2019 16:58:29 +0100 Subject: [PATCH 24/33] Add Exists and DeleteList to ViewRegistry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/viewregistry.go | 38 +++++++++++ sql/viewregistry_test.go | 133 ++++++++++++++++++++++++++++++++------- 2 files changed, 150 insertions(+), 21 deletions(-) diff --git a/sql/viewregistry.go b/sql/viewregistry.go index 77dda35066..d7ebb577cd 100644 --- a/sql/viewregistry.go +++ b/sql/viewregistry.go @@ -90,6 +90,29 @@ func (r *ViewRegistry) Delete(databaseName, viewName string) error { return nil } +// DeleteList tries to delete a list of view keys. +// If the list contains views that do exist and views that do not, the existing +// views are deleted if and only if the errIfNotExists flag is set to false; if +// it is set to true, no views are deleted and an error is returned. +func (r *ViewRegistry) DeleteList(keys []ViewKey, errIfNotExists bool) error { + r.mutex.Lock() + defer r.mutex.Unlock() + + if errIfNotExists { + for _, key := range keys { + if !r.exists(key.dbName, key.viewName) { + return ErrNonExistingView.New(key.dbName, key.viewName) + } + } + } + + for _, key := range keys { + delete(r.views, key) + } + + return nil +} + // View returns a pointer to the view specified by the pair {databaseName, // viewName}, returning an error if it does not exist. func (r *ViewRegistry) View(databaseName, viewName string) (*View, error) { @@ -127,3 +150,18 @@ func (r *ViewRegistry) ViewsInDatabase(databaseName string) (views []View) { return views } + +func (r *ViewRegistry) exists(databaseName, viewName string) bool { + key := NewViewKey(databaseName, viewName) + _, ok := r.views[key] + + return ok +} + +// Exists returns whether the specified key is already registered +func (r *ViewRegistry) Exists(databaseName, viewName string) bool { + r.mutex.RLock() + defer r.mutex.RUnlock() + + return r.exists(databaseName, viewName) +} diff --git a/sql/viewregistry_test.go b/sql/viewregistry_test.go index f33a88c606..adf1ef6c07 100644 --- a/sql/viewregistry_test.go +++ b/sql/viewregistry_test.go @@ -12,6 +12,16 @@ var ( mockView = NewView(viewName, nil) ) +func newMockRegistry(require *require.Assertions) *ViewRegistry { + registry := NewViewRegistry() + + err := registry.Register(dbName, mockView) + require.NoError(err) + require.Equal(1, len(registry.AllViews())) + + return registry +} + // Tests the creation of an empty ViewRegistry with no views registered. func TestNewViewRegistry(t *testing.T) { require := require.New(t) @@ -24,11 +34,7 @@ func TestNewViewRegistry(t *testing.T) { func TestRegisterNonExistingView(t *testing.T) { require := require.New(t) - registry := NewViewRegistry() - - err := registry.Register(dbName, mockView) - require.NoError(err) - require.Equal(1, len(registry.AllViews())) + registry := newMockRegistry(require) actualView, err := registry.View(dbName, viewName) require.NoError(err) @@ -39,13 +45,9 @@ func TestRegisterNonExistingView(t *testing.T) { func TestRegisterExistingVIew(t *testing.T) { require := require.New(t) - registry := NewViewRegistry() + registry := newMockRegistry(require) err := registry.Register(dbName, mockView) - require.NoError(err) - require.Equal(1, len(registry.AllViews())) - - err = registry.Register(dbName, mockView) require.Error(err) require.True(ErrExistingView.Is(err)) } @@ -54,13 +56,9 @@ func TestRegisterExistingVIew(t *testing.T) { func TestDeleteExistingView(t *testing.T) { require := require.New(t) - registry := NewViewRegistry() - - err := registry.Register(dbName, mockView) - require.NoError(err) - require.Equal(1, len(registry.AllViews())) + registry := newMockRegistry(require) - err = registry.Delete(dbName, viewName) + err := registry.Delete(dbName, viewName) require.NoError(err) require.Equal(0, len(registry.AllViews())) } @@ -81,11 +79,7 @@ func TestDeleteNonExistingView(t *testing.T) { func TestGetExistingView(t *testing.T) { require := require.New(t) - registry := NewViewRegistry() - - err := registry.Register(dbName, mockView) - require.NoError(err) - require.Equal(1, len(registry.AllViews())) + registry := newMockRegistry(require) actualView, err := registry.View(dbName, viewName) require.NoError(err) @@ -131,3 +125,100 @@ func TestViewsInDatabase(t *testing.T) { require.Equal(db.numViews, len(views)) } } + +var viewKeys = []ViewKey{ + { + "db1", + "view1", + }, + { + "db1", + "view2", + }, + { + "db2", + "view1", + }, +} + +func registerKeys(registry *ViewRegistry, require *require.Assertions) { + for _, key := range viewKeys { + err := registry.Register(key.dbName, NewView(key.viewName, nil)) + require.NoError(err) + } + require.Equal(len(viewKeys), len(registry.AllViews())) +} + +func TestDeleteExistingList(t *testing.T) { + require := require.New(t) + + test := func(errIfNotExists bool) { + registry := NewViewRegistry() + + registerKeys(registry, require) + err := registry.DeleteList(viewKeys, errIfNotExists) + require.NoError(err) + require.Equal(0, len(registry.AllViews())) + } + + test(true) + test(false) +} + +func TestDeleteNonExistingList(t *testing.T) { + require := require.New(t) + + test := func(errIfNotExists bool) { + registry := NewViewRegistry() + + registerKeys(registry, require) + err := registry.DeleteList([]ViewKey{{"random", "random"}}, errIfNotExists) + if errIfNotExists { + require.Error(err) + } else { + require.NoError(err) + } + require.Equal(len(viewKeys), len(registry.AllViews())) + } + + test(false) + test(true) +} + +func TestDeletePartiallyExistingList(t *testing.T) { + require := require.New(t) + + test := func(errIfNotExists bool) { + registry := NewViewRegistry() + + registerKeys(registry, require) + toDelete := append(viewKeys, ViewKey{"random", "random"}) + err := registry.DeleteList(toDelete, errIfNotExists) + if errIfNotExists { + require.Error(err) + require.Equal(len(viewKeys), len(registry.AllViews())) + } else { + require.NoError(err) + require.Equal(0, len(registry.AllViews())) + } + } + + test(false) + test(true) +} + +func TestExistsOnExistingView(t *testing.T) { + require := require.New(t) + + registry := newMockRegistry(require) + + require.True(registry.Exists(dbName, viewName)) +} + +func TestExistsOnNonExistingView(t *testing.T) { + require := require.New(t) + + registry := newMockRegistry(require) + + require.False(registry.Exists("non", "existing")) +} From f6ff42503eb08bf4511e623f58e679a998361367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Mon, 28 Oct 2019 17:30:25 +0100 Subject: [PATCH 25/33] Parse and analyze DROP VIEW statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/analyzer/assign_catalog.go | 4 + sql/analyzer/assign_catalog_test.go | 6 ++ sql/parse/parse.go | 3 + sql/parse/util.go | 53 ++++++++++ sql/parse/views.go | 46 +++++++++ sql/parse/views_test.go | 74 ++++++++++++++ sql/plan/drop_view.go | 148 ++++++++++++++++++++++++++++ 7 files changed, 334 insertions(+) create mode 100644 sql/plan/drop_view.go diff --git a/sql/analyzer/assign_catalog.go b/sql/analyzer/assign_catalog.go index 1044514549..1accc5ed86 100644 --- a/sql/analyzer/assign_catalog.go +++ b/sql/analyzer/assign_catalog.go @@ -64,6 +64,10 @@ func assignCatalog(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) nc := *node nc.Catalog = a.Catalog return &nc, nil + case *plan.DropView: + nc := *node + nc.Catalog = a.Catalog + return &nc, nil default: return n, nil } diff --git a/sql/analyzer/assign_catalog_test.go b/sql/analyzer/assign_catalog_test.go index a7fd9dff09..22c66059e9 100644 --- a/sql/analyzer/assign_catalog_test.go +++ b/sql/analyzer/assign_catalog_test.go @@ -81,4 +81,10 @@ func TestAssignCatalog(t *testing.T) { cv, ok := node.(*plan.CreateView) require.True(ok) require.Equal(c, cv.Catalog) + + node, err = f.Apply(sql.NewEmptyContext(), a, plan.NewDropView(nil, false)) + require.NoError(err) + dv, ok := node.(*plan.DropView) + require.True(ok) + require.Equal(c, dv.Catalog) } diff --git a/sql/parse/parse.go b/sql/parse/parse.go index aea61f9906..5c516dc76a 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -37,6 +37,7 @@ var ( describeTablesRegex = regexp.MustCompile(`^(describe|desc)\s+table\s+(.*)`) createIndexRegex = regexp.MustCompile(`^create\s+index\s+`) createViewRegex = regexp.MustCompile(`^create\s+(or\s+replace\s+)?view\s+`) + dropViewRegex = regexp.MustCompile(`^drop\s+(if\s+exists\s+)?view\s+`) dropIndexRegex = regexp.MustCompile(`^drop\s+index\s+`) showIndexRegex = regexp.MustCompile(`^show\s+(index|indexes|keys)\s+(from|in)\s+\S+\s*`) showCreateRegex = regexp.MustCompile(`^show create\s+\S+\s*`) @@ -84,6 +85,8 @@ func Parse(ctx *sql.Context, query string) (sql.Node, error) { return parseCreateIndex(ctx, s) case createViewRegex.MatchString(lowerQuery): return parseCreateView(ctx, s) + case dropViewRegex.MatchString(lowerQuery): + return parseDropView(ctx, s) case dropIndexRegex.MatchString(lowerQuery): return parseDropIndex(s) case showIndexRegex.MatchString(lowerQuery): diff --git a/sql/parse/util.go b/sql/parse/util.go index b438ad3493..6b12ae0cf9 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -516,3 +516,56 @@ func maybeList(opening, separator, closing rune, list *[]string) parseFunc { } } } + +type QualifiedName struct { + db string + name string +} + +func readQualifiedIdentifierList(list *[]QualifiedName) parseFunc { + return func(rd *bufio.Reader) error { + for { + var newItem []string + err := parseFuncs{ + skipSpaces, + readIdentList('.', &newItem), + skipSpaces, + }.exec(rd) + + if err != nil { + return err + } + + if len(newItem) < 1 || len(newItem) > 2 { + return errUnexpectedSyntax.New("[db_name.]viewName", strings.Join(newItem, ".")) + } + + var db, name string + + if len(newItem) == 1 { + db = "" + name = newItem[0] + } else { + db = newItem[0] + name = newItem[1] + } + + *list = append(*list, QualifiedName{db, name}) + + r, _, err := rd.ReadRune() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + switch r { + case ',': + continue + default: + return rd.UnreadRune() + } + } + } +} diff --git a/sql/parse/views.go b/sql/parse/views.go index 11b7bbf2fb..1f40e4d794 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -13,6 +13,7 @@ import ( var ErrMalformedViewName = errors.NewKind("the view name '%s' is not correct") var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT query") +var ErrViewsToDropNotFound = errors.NewKind("the list of views to drop must contain at least one view") // parseCreateView parses // CREATE [OR REPLACE] VIEW [db_name.]view_name AS select_statement @@ -87,3 +88,48 @@ func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { sql.UnresolvedDatabase(databaseName), viewName, columns, subqueryAlias, isReplace, ), nil } + +// parseDropView parses +// DROP VIEW [IF EXISTS] [db_name1.]view_name1 [, [db_name2.]view_name2, ...] +// [RESTRICT] [CASCADE] +// and returns a DropView node in case of success. As per MySQL specification, +// RESTRICT and CASCADE, if given, are parsed and ignored. +func parseDropView(ctx *sql.Context, s string) (sql.Node, error) { + r := bufio.NewReader(strings.NewReader(s)) + + var ( + views []QualifiedName + ifExists bool + unusedBool bool + ) + + err := parseFuncs{ + expect("drop"), + skipSpaces, + expect("view"), + skipSpaces, + multiMaybe(&ifExists, "if", "exists"), + skipSpaces, + readQualifiedIdentifierList(&views), + skipSpaces, + maybe(&unusedBool, "restrict"), + skipSpaces, + maybe(&unusedBool, "cascade"), + checkEOF, + }.exec(r) + + if err != nil { + return nil, err + } + + if len(views) < 1 { + return nil, ErrViewsToDropNotFound.New() + } + + plans := make([]sql.Node, len(views)) + for i, view := range views { + plans[i] = plan.NewSingleDropView(sql.UnresolvedDatabase(view.db), view.name) + } + + return plan.NewDropView(plans, ifExists), nil +} diff --git a/sql/parse/views_test.go b/sql/parse/views_test.go index 16b0e38b18..c0a5b53f66 100644 --- a/sql/parse/views_test.go +++ b/sql/parse/views_test.go @@ -75,3 +75,77 @@ func TestParseCreateView(t *testing.T) { }) } } + +func TestParseDropView(t *testing.T) { + var fixtures = map[string]sql.Node{ + `DROP VIEW view1`: plan.NewDropView( + []sql.Node{plan.NewSingleDropView(sql.UnresolvedDatabase(""), "view1")}, + false, + ), + `DROP VIEW view1, view2`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase(""), "view1"), + plan.NewSingleDropView(sql.UnresolvedDatabase(""), "view2"), + }, + false, + ), + `DROP VIEW db1.view1`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase("db1"), "view1"), + }, + false, + ), + `DROP VIEW db1.view1, view2`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase("db1"), "view1"), + plan.NewSingleDropView(sql.UnresolvedDatabase(""), "view2"), + }, + false, + ), + `DROP VIEW view1, db2.view2`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase(""), "view1"), + plan.NewSingleDropView(sql.UnresolvedDatabase("db2"), "view2"), + }, + false, + ), + `DROP VIEW db1.view1, db2.view2`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase("db1"), "view1"), + plan.NewSingleDropView(sql.UnresolvedDatabase("db2"), "view2"), + }, + false, + ), + `DROP VIEW IF EXISTS myview`: plan.NewDropView( + []sql.Node{plan.NewSingleDropView(sql.UnresolvedDatabase(""), "myview")}, + true, + ), + `DROP VIEW IF EXISTS db1.view1, db2.view2`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase("db1"), "view1"), + plan.NewSingleDropView(sql.UnresolvedDatabase("db2"), "view2"), + }, + true, + ), + `DROP VIEW IF EXISTS db1.view1, db2.view2 RESTRICT CASCADE`: plan.NewDropView( + []sql.Node{ + plan.NewSingleDropView(sql.UnresolvedDatabase("db1"), "view1"), + plan.NewSingleDropView(sql.UnresolvedDatabase("db2"), "view2"), + }, + true, + ), + } + + for query, expectedPlan := range fixtures { + t.Run(query, func(t *testing.T) { + require := require.New(t) + + ctx := sql.NewEmptyContext() + lowerquery := strings.ToLower(query) + result, err := parseDropView(ctx, lowerquery) + + require.NoError(err) + require.Equal(expectedPlan, result) + }) + } +} diff --git a/sql/plan/drop_view.go b/sql/plan/drop_view.go new file mode 100644 index 0000000000..718d2bfcde --- /dev/null +++ b/sql/plan/drop_view.go @@ -0,0 +1,148 @@ +package plan + +import ( + "github.com/src-d/go-mysql-server/sql" + errors "gopkg.in/src-d/go-errors.v1" +) + +var errDropViewChild = errors.NewKind("any child of DropView must be of type SingleDropView") + +type SingleDropView struct { + database sql.Database + viewName string +} + +// NewSingleDropView creates a SingleDropView. +func NewSingleDropView( + database sql.Database, + viewName string, +) *SingleDropView { + return &SingleDropView{database, viewName} +} + +// Children implements the Node interface. It always returns nil. +func (dv *SingleDropView) Children() []sql.Node { + return nil +} + +// Resolved implements the Node interface. This node is resolved if and only if +// its database is resolved. +func (dv *SingleDropView) Resolved() bool { + _, ok := dv.database.(sql.UnresolvedDatabase) + return !ok +} + +// RowIter implements the Node interface. It always returns an empty iterator. +func (dv *SingleDropView) RowIter(ctx *sql.Context) (sql.RowIter, error) { + return sql.RowsToRowIter(), nil +} + +// Schema implements the Node interface. It always returns nil. +func (dv *SingleDropView) Schema() sql.Schema { return nil } + +// String implements the fmt.Stringer interface, using sql.TreePrinter to +// generate the string. +func (dv *SingleDropView) String() string { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("SingleDropView(%s.%s)", dv.database.Name(), dv.viewName) + + return pr.String() +} + +// WithChildren implements the Node interface. It only succeeds if the length +// of the specified children equals 0. +func (dv *SingleDropView) WithChildren(children ...sql.Node) (sql.Node, error) { + if len(children) != 0 { + return nil, sql.ErrInvalidChildrenNumber.New(dv, len(children), 0) + } + + return dv, nil +} + +// Database implements the Databaser interfacee. It returns the node's database. +func (dv *SingleDropView) Database() sql.Database { + return dv.database +} + +// Database implements the Databaser interface, and it returns a copy of this +// node with the specified database. +func (dv *SingleDropView) WithDatabase(database sql.Database) (sql.Node, error) { + newDrop := *dv + newDrop.database = database + return &newDrop, nil +} + +// DropView is a node representing the removal of a list of views, defined by +// the children member. The flag ifExists represents whether the user wants the +// node to fail if any of the views in children does not exist. +type DropView struct { + children []sql.Node + Catalog *sql.Catalog + ifExists bool +} + +// NewDropView creates a DropView node with the specified parameters, +// setting its catalog to nil. +func NewDropView(children []sql.Node, ifExists bool) *DropView { + return &DropView{children, nil, ifExists} +} + +// Children implements the Node interface. It returns the children of the +// CreateView node; i.e., all the views that will be dropped. +func (dvs *DropView) Children() []sql.Node { + return dvs.children +} + +// Resolved implements the Node interface. This node is resolved if and only if +// all of its children are resolved. +func (dvs *DropView) Resolved() bool { + for _, child := range dvs.children { + if !child.Resolved() { + return false + } + } + return true +} + +// RowIter implements the Node interface. When executed, this function drops +// all the views defined by the node's children. It errors if the flag ifExists +// is set to false and there is some view that does not exist. +func (dvs *DropView) RowIter(ctx *sql.Context) (sql.RowIter, error) { + viewList := make([]sql.ViewKey, len(dvs.children)) + for i, child := range dvs.children { + drop, ok := child.(*SingleDropView) + if !ok { + return sql.RowsToRowIter(), errDropViewChild.New() + } + + viewList[i] = sql.NewViewKey(drop.database.Name(), drop.viewName) + } + + return sql.RowsToRowIter(), dvs.Catalog.ViewRegistry.DeleteList(viewList, !dvs.ifExists) +} + +// Schema implements the Node interface. It always returns nil. +func (dvs *DropView) Schema() sql.Schema { return nil } + +// String implements the fmt.Stringer interface, using sql.TreePrinter to +// generate the string. +func (dvs *DropView) String() string { + childrenStrings := make([]string, len(dvs.children)) + for i, child := range dvs.children { + childrenStrings[i] = child.String() + } + + pr := sql.NewTreePrinter() + _ = pr.WriteNode("DropView") + _ = pr.WriteChildren(childrenStrings...) + + return pr.String() +} + +// WithChildren implements the Node interface. It always suceeds, returning a +// copy of this node with the new array of nodes as children. +func (dvs *DropView) WithChildren(children ...sql.Node) (sql.Node, error) { + newDrop := dvs + newDrop.children = children + return newDrop, nil +} From 889bd1e4fc13c27536fe3fd19fee298854bcd57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 29 Oct 2019 10:08:14 +0100 Subject: [PATCH 26/33] Test parsing utility readQualifiedIdentifierList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 23 +++++++++++------ sql/parse/util_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++ sql/parse/views.go | 2 +- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index 6b12ae0cf9..b73d927aee 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -517,11 +517,17 @@ func maybeList(opening, separator, closing rune, list *[]string) parseFunc { } } +// A QualifiedName represents an identifier of type "db_name.table_name" type QualifiedName struct { - db string - name string + qualifier string + name string } +// readQualifiedIdentifierList reads a comma-separated list of QualifiedNames. +// Any number of spaces between the qualified names are accepted. The qualifier +// may be empty, in which case the period is optional. +// An example of a correctly formed list is: +// "my_db.myview, db_2.mytable , aTable" func readQualifiedIdentifierList(list *[]QualifiedName) parseFunc { return func(rd *bufio.Reader) error { for { @@ -537,20 +543,23 @@ func readQualifiedIdentifierList(list *[]QualifiedName) parseFunc { } if len(newItem) < 1 || len(newItem) > 2 { - return errUnexpectedSyntax.New("[db_name.]viewName", strings.Join(newItem, ".")) + return errUnexpectedSyntax.New( + "[qualifier.]name", + strings.Join(newItem, "."), + ) } - var db, name string + var qualifier, name string if len(newItem) == 1 { - db = "" + qualifier = "" name = newItem[0] } else { - db = newItem[0] + qualifier = newItem[0] name = newItem[1] } - *list = append(*list, QualifiedName{db, name}) + *list = append(*list, QualifiedName{qualifier, name}) r, _, err := rd.ReadRune() if err != nil { diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go index b1cd07fe59..eaaf43e747 100644 --- a/sql/parse/util_test.go +++ b/sql/parse/util_test.go @@ -465,3 +465,60 @@ func TestReadSpaces(t *testing.T) { require.Equal(fixture.expectedRemaining, actualRemaining) } } + +// Tests that readQualifiedIdentifierList correctly parses well-formed lists, +// populating the list of identifiers, and that it errors with partial lists +// and when it does not found any identifiers +func TestReadQualifiedIdentifierList(t *testing.T) { + require := require.New(t) + + testFixtures := []struct { + string string + expectedList []QualifiedName + expectedError bool + expectedRemaining string + }{ + { + "my_db.myview, db_2.mytable , aTable", + []QualifiedName{{"my_db", "myview"}, {"db_2", "mytable"}, {"", "aTable"}}, + false, + "", + }, + { + "single_identifier -remaining", + []QualifiedName{{"", "single_identifier"}}, + false, + "-remaining", + }, + { + "", + nil, + true, + "", + }, + { + "partial_list,", + []QualifiedName{{"", "partial_list"}}, + true, + "", + }, + } + + for _, fixture := range testFixtures { + reader := bufio.NewReader(strings.NewReader(fixture.string)) + var actualList []QualifiedName + + err := readQualifiedIdentifierList(&actualList)(reader) + + if fixture.expectedError { + require.Error(err) + } else { + require.NoError(err) + } + + require.Equal(fixture.expectedList, actualList) + + actualRemaining, _ := reader.ReadString('\n') + require.Equal(fixture.expectedRemaining, actualRemaining) + } +} diff --git a/sql/parse/views.go b/sql/parse/views.go index 1f40e4d794..d8fb948f2a 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -128,7 +128,7 @@ func parseDropView(ctx *sql.Context, s string) (sql.Node, error) { plans := make([]sql.Node, len(views)) for i, view := range views { - plans[i] = plan.NewSingleDropView(sql.UnresolvedDatabase(view.db), view.name) + plans[i] = plan.NewSingleDropView(sql.UnresolvedDatabase(view.qualifier), view.name) } return plan.NewDropView(plans, ifExists), nil From 38bf07f1466939c8a50309ba2abcc409ec907a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 29 Oct 2019 12:19:55 +0100 Subject: [PATCH 27/33] Test DropView node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/plan/drop_view_test.go | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 sql/plan/drop_view_test.go diff --git a/sql/plan/drop_view_test.go b/sql/plan/drop_view_test.go new file mode 100644 index 0000000000..0f9287fcc7 --- /dev/null +++ b/sql/plan/drop_view_test.go @@ -0,0 +1,94 @@ +package plan + +import ( + "testing" + + "github.com/src-d/go-mysql-server/memory" + "github.com/src-d/go-mysql-server/sql" + "github.com/src-d/go-mysql-server/sql/expression" + + "github.com/stretchr/testify/require" +) + +// Generates a database with a single table called mytable and a catalog with +// the view that is also returned. The context returned is the one used to +// create the view. +func mockData(require *require.Assertions) (sql.Database, *sql.Catalog, *sql.Context, sql.View) { + table := memory.NewTable("mytable", sql.Schema{ + {Name: "i", Source: "mytable", Type: sql.Int32}, + {Name: "s", Source: "mytable", Type: sql.Text}, + }) + + db := memory.NewDatabase("db") + db.AddTable("db", table) + + catalog := sql.NewCatalog() + catalog.AddDatabase(db) + + subqueryAlias := NewSubqueryAlias("myview", + NewProject( + []sql.Expression{ + expression.NewGetFieldWithTable(1, sql.Int32, table.Name(), "i", true), + }, + NewUnresolvedTable("dual", ""), + ), + ) + + createView := NewCreateView(db, subqueryAlias.Name(), nil, subqueryAlias, false) + createView.Catalog = catalog + + ctx := sql.NewEmptyContext() + + _, err := createView.RowIter(ctx) + require.NoError(err) + + return db, catalog, ctx, createView.View() +} + +// Tests that DropView works as expected and that the view is dropped in +// the catalog when RowIter is called, regardless of the value of ifExists +func TestDropExistingView(t *testing.T) { + require := require.New(t) + + test := func(ifExists bool) { + db, catalog, ctx, view := mockData(require) + + singleDropView := NewSingleDropView(db, view.Name()) + dropView := NewDropView([]sql.Node{singleDropView}, ifExists) + dropView.Catalog = catalog + + _, err := dropView.RowIter(ctx) + require.NoError(err) + + require.False(catalog.ViewRegistry.Exists(db.Name(), view.Name())) + } + + test(false) + test(true) +} + +// Tests that DropView errors when trying to delete a non-existing view if and +// only if the flag ifExists is set to false +func TestDropNonExistingView(t *testing.T) { + require := require.New(t) + + test := func(ifExists bool) error { + db, catalog, ctx, view := mockData(require) + + singleDropView := NewSingleDropView(db, "non-existing-view") + dropView := NewDropView([]sql.Node{singleDropView}, ifExists) + dropView.Catalog = catalog + + _, err := dropView.RowIter(ctx) + + require.True(catalog.ViewRegistry.Exists(db.Name(), view.Name())) + + return err + } + + err := test(true) + require.NoError(err) + + err = test(false) + require.Error(err) +} From cb3b61c0c17e9a327c387097a824739bb2ad0925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 29 Oct 2019 12:36:17 +0100 Subject: [PATCH 28/33] Make sure that DropView is not executed without write permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- engine.go | 2 +- engine_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/engine.go b/engine.go index a622f2b24b..336be44207 100644 --- a/engine.go +++ b/engine.go @@ -120,7 +120,7 @@ func (e *Engine) Query( case *plan.CreateIndex: typ = sql.CreateIndexProcess perm = auth.ReadPerm | auth.WritePerm - case *plan.InsertInto, *plan.DeleteFrom, *plan.Update, *plan.DropIndex, *plan.UnlockTables, *plan.LockTables, *plan.CreateView: + case *plan.InsertInto, *plan.DeleteFrom, *plan.Update, *plan.DropIndex, *plan.UnlockTables, *plan.LockTables, *plan.CreateView, *plan.DropView: perm = auth.ReadPerm | auth.WritePerm } diff --git a/engine_test.go b/engine_test.go index 8846d0ebdd..be229f91c5 100644 --- a/engine_test.go +++ b/engine_test.go @@ -3149,6 +3149,7 @@ func TestReadOnly(t *testing.T) { `DROP INDEX foo ON mytable`, `INSERT INTO mytable (i, s) VALUES(42, 'yolo')`, `CREATE VIEW myview AS SELECT i FROM mytable`, + `DROP VIEW myview`, } for _, query := range writingQueries { From 9b8ecfaa7946a9b87e74946736b6566c205f6f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Tue, 29 Oct 2019 15:02:43 +0100 Subject: [PATCH 29/33] Make qualifiedName type private to parse package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alejandro García Montoro --- sql/parse/util.go | 10 +++++----- sql/parse/util_test.go | 10 +++++----- sql/parse/views.go | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sql/parse/util.go b/sql/parse/util.go index b73d927aee..b14375a2af 100644 --- a/sql/parse/util.go +++ b/sql/parse/util.go @@ -517,18 +517,18 @@ func maybeList(opening, separator, closing rune, list *[]string) parseFunc { } } -// A QualifiedName represents an identifier of type "db_name.table_name" -type QualifiedName struct { +// A qualifiedName represents an identifier of type "db_name.table_name" +type qualifiedName struct { qualifier string name string } -// readQualifiedIdentifierList reads a comma-separated list of QualifiedNames. +// readQualifiedIdentifierList reads a comma-separated list of qualifiedNames. // Any number of spaces between the qualified names are accepted. The qualifier // may be empty, in which case the period is optional. // An example of a correctly formed list is: // "my_db.myview, db_2.mytable , aTable" -func readQualifiedIdentifierList(list *[]QualifiedName) parseFunc { +func readQualifiedIdentifierList(list *[]qualifiedName) parseFunc { return func(rd *bufio.Reader) error { for { var newItem []string @@ -559,7 +559,7 @@ func readQualifiedIdentifierList(list *[]QualifiedName) parseFunc { name = newItem[1] } - *list = append(*list, QualifiedName{qualifier, name}) + *list = append(*list, qualifiedName{qualifier, name}) r, _, err := rd.ReadRune() if err != nil { diff --git a/sql/parse/util_test.go b/sql/parse/util_test.go index eaaf43e747..3cb5f49f8b 100644 --- a/sql/parse/util_test.go +++ b/sql/parse/util_test.go @@ -474,19 +474,19 @@ func TestReadQualifiedIdentifierList(t *testing.T) { testFixtures := []struct { string string - expectedList []QualifiedName + expectedList []qualifiedName expectedError bool expectedRemaining string }{ { "my_db.myview, db_2.mytable , aTable", - []QualifiedName{{"my_db", "myview"}, {"db_2", "mytable"}, {"", "aTable"}}, + []qualifiedName{{"my_db", "myview"}, {"db_2", "mytable"}, {"", "aTable"}}, false, "", }, { "single_identifier -remaining", - []QualifiedName{{"", "single_identifier"}}, + []qualifiedName{{"", "single_identifier"}}, false, "-remaining", }, @@ -498,7 +498,7 @@ func TestReadQualifiedIdentifierList(t *testing.T) { }, { "partial_list,", - []QualifiedName{{"", "partial_list"}}, + []qualifiedName{{"", "partial_list"}}, true, "", }, @@ -506,7 +506,7 @@ func TestReadQualifiedIdentifierList(t *testing.T) { for _, fixture := range testFixtures { reader := bufio.NewReader(strings.NewReader(fixture.string)) - var actualList []QualifiedName + var actualList []qualifiedName err := readQualifiedIdentifierList(&actualList)(reader) diff --git a/sql/parse/views.go b/sql/parse/views.go index d8fb948f2a..d543952bf8 100644 --- a/sql/parse/views.go +++ b/sql/parse/views.go @@ -98,7 +98,7 @@ func parseDropView(ctx *sql.Context, s string) (sql.Node, error) { r := bufio.NewReader(strings.NewReader(s)) var ( - views []QualifiedName + views []qualifiedName ifExists bool unusedBool bool ) From a21705789adc1197839641457c771811bb14573e Mon Sep 17 00:00:00 2001 From: Aaron Son Date: Thu, 21 Nov 2019 15:16:26 -0800 Subject: [PATCH 30/33] Cleanup for vitess parser changes. --- go.mod | 7 ++- go.sum | 130 ++++++++++++++++++++++++++++++++++++++++ sql/parse/parse.go | 27 +++++++-- sql/parse/parse_test.go | 2 +- sql/parse/views.go | 89 --------------------------- sql/parse/views_test.go | 77 ------------------------ 6 files changed, 158 insertions(+), 174 deletions(-) delete mode 100644 sql/parse/views.go delete mode 100644 sql/parse/views_test.go diff --git a/go.mod b/go.mod index e8aab26b70..2d03b30e72 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/go-sql-driver/mysql v1.4.1 github.com/gogo/protobuf v1.2.1 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/golang/protobuf v1.3.0 // indirect github.com/hashicorp/golang-lru v0.5.3 github.com/mitchellh/hashstructure v1.0.0 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 @@ -21,8 +20,12 @@ require ( github.com/src-d/go-oniguruma v1.0.0 github.com/stretchr/testify v1.3.0 go.etcd.io/bbolt v1.3.2 - google.golang.org/grpc v1.19.0 // indirect + google.golang.org/grpc v1.25.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 gopkg.in/yaml.v2 v2.2.2 vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible ) + +replace vitess.io/vitess => ../vitess + +go 1.13 diff --git a/go.sum b/go.sum index c2918275d1..76dab07176 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ 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= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d h1:n0G4ckjMEj7bWuGYUX0i8YlBeBBJuZ+HEHvHfyBDZtI= @@ -14,20 +16,29 @@ github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/aws/aws-sdk-go v0.0.0-20180223184012-ebef4262e06a/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= +github.com/beorn7/perks v0.0.0-20160229213445-3ac7bf7a47d1/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/etcd v0.0.0-20170626015032-703663d1f6ed/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20161207003320-04f313413ffd/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-ini/ini v1.12.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= @@ -39,75 +50,125 @@ github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.3.0 h1:tsg9qP3mjt1h4Roxp+M1paRjrVBfPSOpBuVclh6YluI= github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20160912153041-2d1e4548da23/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20180418170936-39de4380c2e0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20161128002007-199c40a060d1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/consul v1.4.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.0.0-20160407174126-ad28ea4487f0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.0.0-20161207011743-d3a67ab21bc8/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v0.0.0-20180801095237-b50017755d44/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/crc32 v1.2.0/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.2.0/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-runewidth v0.0.1/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/minio-go v0.0.0-20190131015406-c8a261de75c1/go.mod h1:vuvdOZLJuf5HmJAJrKV64MmozrSsk+or0PB5dzdfspg= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/olekukonko/tablewriter v0.0.0-20160115111002-cca8bbc07984/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= +github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v0.0.0-20160824210600-b984ec7fa9ff/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pilosa/pilosa v1.3.0 h1:P27JB4tIqAN4Yc2Fw7wS5neD7JNkFKRUmwfyV87JMwQ= github.com/pilosa/pilosa v1.3.0/go.mod h1:97yLL9mpUqOj9naKu5XA/b/U6JLe3JGGUlc2HOTDw+A= +github.com/pkg/errors v0.0.0-20190109061628-ffb6e22f0193/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.0.0-20180319131721-d49167c4b9f3/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20150212101744-fa8ad6fec335/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20160607094339-3a184ff7dfd4/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20160411190841-abf152e5f3e9/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 h1:YDeskXpkNDhPdWN3REluVa46HQOVuVkjkd2sWnrABNQ= github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/samuel/go-zookeeper v0.0.0-20160616024954-e64db453f351/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanity-io/litter v1.1.0 h1:BllcKWa3VbZmOZbDCoszYLk7zCsKHz5Beossi8SUcTc= github.com/sanity-io/litter v1.1.0/go.mod h1:CJ0VCw2q4qKU7LaQr3n7UOSHzgEMgcGco7N/SkZQPjw= +github.com/satori/go.uuid v0.0.0-20160713180306-0aa62d5ddceb/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v0.0.0-20170409071739-feef008d51ad/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM= github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -121,56 +182,125 @@ github.com/src-d/go-oniguruma v1.0.0 h1:JDk5PUAjreGsGAKLsoDLNmrsaryjJ5RqT3h+Si6a github.com/src-d/go-oniguruma v1.0.0/go.mod h1:chVbff8kcVtmrhxtZ3yBVLLquXbzCS6DrxQaAK/CeqM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20160524234229-8d64eb7173c7/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tchap/go-patricia v0.0.0-20160729071656-dd168db6051b/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk= github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uber/jaeger-lib v2.0.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yudai/gojsondiff v0.0.0-20170626131258-081cda2ee950/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 h1:x6r4Jo0KNzOOzYd8lbcRsqjuqEASK6ob3auvWYM4/8U= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ldap.v2 v2.5.0/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= +gopkg.in/yaml.v2 v2.0.0-20160928153709-a5b47d31c556/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= modernc.org/mathutil v1.0.0 h1:93vKjrJopTPrtTNpZ8XIovER7iCIH1QU7wNbOQXC60I= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0 h1:XVFtQwFVwc02Wk+0L/Z/zDDXO81r5Lhe6iMKmGX3KhE= diff --git a/sql/parse/parse.go b/sql/parse/parse.go index aea61f9906..9b7c70ce54 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -36,7 +36,6 @@ var ( var ( describeTablesRegex = regexp.MustCompile(`^(describe|desc)\s+table\s+(.*)`) createIndexRegex = regexp.MustCompile(`^create\s+index\s+`) - createViewRegex = regexp.MustCompile(`^create\s+(or\s+replace\s+)?view\s+`) dropIndexRegex = regexp.MustCompile(`^drop\s+index\s+`) showIndexRegex = regexp.MustCompile(`^show\s+(index|indexes|keys)\s+(from|in)\s+\S+\s*`) showCreateRegex = regexp.MustCompile(`^show create\s+\S+\s*`) @@ -82,8 +81,6 @@ func Parse(ctx *sql.Context, query string) (sql.Node, error) { return parseDescribeTables(lowerQuery) case createIndexRegex.MatchString(lowerQuery): return parseCreateIndex(ctx, s) - case createViewRegex.MatchString(lowerQuery): - return parseCreateView(ctx, s) case dropIndexRegex.MatchString(lowerQuery): return parseDropIndex(s) case showIndexRegex.MatchString(lowerQuery): @@ -162,7 +159,7 @@ func convert(ctx *sql.Context, stmt sqlparser.Statement, query string) (sql.Node if err != nil { return nil, err } - return convertDDL(ddl.(*sqlparser.DDL)) + return convertDDL(ctx, ddl.(*sqlparser.DDL)) case *sqlparser.Set: return convertSet(ctx, n) case *sqlparser.Use: @@ -368,9 +365,12 @@ func convertSelect(ctx *sql.Context, s *sqlparser.Select) (sql.Node, error) { return node, nil } -func convertDDL(c *sqlparser.DDL) (sql.Node, error) { +func convertDDL(ctx *sql.Context, c *sqlparser.DDL) (sql.Node, error) { switch c.Action { case sqlparser.CreateStr: + if !c.View.IsEmpty() { + return convertCreateView(ctx, c) + } return convertCreateTable(c) case sqlparser.DropStr: return convertDropTable(c) @@ -397,6 +397,23 @@ func convertCreateTable(c *sqlparser.DDL) (sql.Node, error) { sql.UnresolvedDatabase(""), c.Table.Name.String(), schema), nil } +func convertCreateView(ctx *sql.Context, c *sqlparser.DDL) (sql.Node, error) { + selectStatement, ok := c.ViewExpr.(*sqlparser.Select) + if !ok { + return nil, ErrUnsupportedSyntax.New(c.ViewExpr) + } + + queryNode, err := convertSelect(ctx, selectStatement) + if err != nil { + return nil, err + } + + queryAlias := plan.NewSubqueryAlias(c.View.Name.String(), queryNode) + + return plan.NewCreateView( + sql.UnresolvedDatabase(""), c.View.Name.String(), []string{}, queryAlias, c.OrReplace), nil +} + func convertInsert(ctx *sql.Context, i *sqlparser.Insert) (sql.Node, error) { if len(i.OnDup) > 0 { return nil, ErrUnsupportedFeature.New("ON DUPLICATE KEY") diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go index 1d8b5f49e2..7f60c6c634 100644 --- a/sql/parse/parse_test.go +++ b/sql/parse/parse_test.go @@ -1296,7 +1296,7 @@ var fixturesErrors = map[string]*errors.Kind{ `SELECT INTERVAL 1 DAY + INTERVAL 1 DAY`: ErrUnsupportedSyntax, `SELECT '2018-05-01' + (INTERVAL 1 DAY + INTERVAL 1 DAY)`: ErrUnsupportedSyntax, `SELECT AVG(DISTINCT foo) FROM b`: ErrUnsupportedSyntax, - `CREATE VIEW myview (col1) AS SELECT 1`: ErrUnsupportedSyntax, + `CREATE VIEW myview AS SELECT AVG(DISTINCT foo) FROM b`: ErrUnsupportedSyntax, } func TestParseErrors(t *testing.T) { diff --git a/sql/parse/views.go b/sql/parse/views.go deleted file mode 100644 index 11b7bbf2fb..0000000000 --- a/sql/parse/views.go +++ /dev/null @@ -1,89 +0,0 @@ -package parse - -import ( - "bufio" - "strings" - - "github.com/src-d/go-mysql-server/sql" - "github.com/src-d/go-mysql-server/sql/plan" - - "gopkg.in/src-d/go-errors.v1" - "vitess.io/vitess/go/vt/sqlparser" -) - -var ErrMalformedViewName = errors.NewKind("the view name '%s' is not correct") -var ErrMalformedCreateView = errors.NewKind("view definition %#v is not a SELECT query") - -// parseCreateView parses -// CREATE [OR REPLACE] VIEW [db_name.]view_name AS select_statement -// and returns a NewCreateView node in case of success -func parseCreateView(ctx *sql.Context, s string) (sql.Node, error) { - r := bufio.NewReader(strings.NewReader(s)) - - var ( - databaseName, viewName string - scopedName []string - subquery string - columns []string - isReplace bool - ) - - err := parseFuncs{ - expect("create"), - skipSpaces, - multiMaybe(&isReplace, "or", "replace"), - skipSpaces, - expect("view"), - skipSpaces, - readIdentList('.', &scopedName), - skipSpaces, - maybeList('(', ',', ')', &columns), - skipSpaces, - expect("as"), - skipSpaces, - readRemaining(&subquery), - checkEOF, - }.exec(r) - - if err != nil { - return nil, err - } - - if len(scopedName) < 1 || len(scopedName) > 2 { - return nil, ErrMalformedViewName.New(strings.Join(scopedName, ".")) - } - - // TODO(agarciamontoro): Add support for explicit column names - if len(columns) != 0 { - return nil, ErrUnsupportedSyntax.New("the view creation must not specify explicit column names") - } - - if len(scopedName) == 1 { - viewName = scopedName[0] - } - if len(scopedName) == 2 { - databaseName = scopedName[0] - viewName = scopedName[1] - } - - subqueryStatement, err := sqlparser.Parse(subquery) - if err != nil { - return nil, err - } - - selectStatement, ok := subqueryStatement.(*sqlparser.Select) - if !ok { - return nil, ErrMalformedCreateView.New(subqueryStatement) - } - - subqueryNode, err := convertSelect(ctx, selectStatement) - if err != nil { - return nil, err - } - - subqueryAlias := plan.NewSubqueryAlias(viewName, subqueryNode) - - return plan.NewCreateView( - sql.UnresolvedDatabase(databaseName), viewName, columns, subqueryAlias, isReplace, - ), nil -} diff --git a/sql/parse/views_test.go b/sql/parse/views_test.go deleted file mode 100644 index 16b0e38b18..0000000000 --- a/sql/parse/views_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package parse - -import ( - "strings" - "testing" - - "github.com/src-d/go-mysql-server/sql" - "github.com/src-d/go-mysql-server/sql/expression" - "github.com/src-d/go-mysql-server/sql/plan" - "github.com/stretchr/testify/require" -) - -func TestParseCreateView(t *testing.T) { - var fixtures = map[string]sql.Node{ - `CREATE VIEW myview AS SELECT 1`: plan.NewCreateView( - sql.UnresolvedDatabase(""), - "myview", - nil, - plan.NewSubqueryAlias("myview", - plan.NewProject( - []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, - plan.NewUnresolvedTable("dual", ""), - ), - ), - false, - ), - `CREATE VIEW myview AS SELECT * FROM mytable`: plan.NewCreateView( - sql.UnresolvedDatabase(""), - "myview", - nil, - plan.NewSubqueryAlias("myview", - plan.NewProject( - []sql.Expression{expression.NewStar()}, - plan.NewUnresolvedTable("mytable", ""), - ), - ), - false, - ), - `CREATE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( - sql.UnresolvedDatabase("mydb"), - "myview", - nil, - plan.NewSubqueryAlias("myview", - plan.NewProject( - []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, - plan.NewUnresolvedTable("dual", ""), - ), - ), - false, - ), - `CREATE OR REPLACE VIEW mydb.myview AS SELECT 1`: plan.NewCreateView( - sql.UnresolvedDatabase("mydb"), - "myview", - nil, - plan.NewSubqueryAlias("myview", - plan.NewProject( - []sql.Expression{expression.NewLiteral(int8(1), sql.Int8)}, - plan.NewUnresolvedTable("dual", ""), - ), - ), - true, - ), - } - - for query, expectedPlan := range fixtures { - t.Run(query, func(t *testing.T) { - require := require.New(t) - - ctx := sql.NewEmptyContext() - lowerquery := strings.ToLower(query) - result, err := parseCreateView(ctx, lowerquery) - - require.NoError(err) - require.Equal(expectedPlan, result) - }) - } -} From 51e0cedee3e57a1752f1d7438186f0a8c1aaadf1 Mon Sep 17 00:00:00 2001 From: Aaron Son Date: Thu, 21 Nov 2019 15:43:54 -0800 Subject: [PATCH 31/33] Fix DROP VIEW parsing. --- sql/parse/parse.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sql/parse/parse.go b/sql/parse/parse.go index 68bf06f38f..22f0d191e3 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -373,6 +373,9 @@ func convertDDL(ctx *sql.Context, c *sqlparser.DDL) (sql.Node, error) { } return convertCreateTable(c) case sqlparser.DropStr: + if len(c.FromViews) != 0 { + return convertDropView(ctx, c) + } return convertDropTable(c) default: return nil, ErrUnsupportedSyntax.New(c) From 9da9885f7ed097fbda7c57e8ec6241f7ff434428 Mon Sep 17 00:00:00 2001 From: Aaron Son Date: Mon, 25 Nov 2019 14:12:01 -0800 Subject: [PATCH 32/33] go.mod: Keep go.mod looking like ld-master for now. --- go.mod | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) mode change 100644 => 100755 go.mod diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 index 2d03b30e72..1a61e143ae --- a/go.mod +++ b/go.mod @@ -1,31 +1,30 @@ module github.com/src-d/go-mysql-server require ( - github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/go-kit/kit v0.8.0 - github.com/go-ole/go-ole v1.2.4 // indirect github.com/go-sql-driver/mysql v1.4.1 - github.com/gogo/protobuf v1.2.1 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/google/btree v1.0.0 // indirect + github.com/google/go-cmp v0.3.0 // indirect github.com/hashicorp/golang-lru v0.5.3 github.com/mitchellh/hashstructure v1.0.0 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 - github.com/opentracing/opentracing-go v1.0.2 - github.com/pilosa/pilosa v1.3.0 - github.com/sanity-io/litter v1.1.0 - github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect - github.com/sirupsen/logrus v1.3.0 + github.com/opentracing/opentracing-go v1.1.0 + github.com/pilosa/pilosa v1.4.0 + github.com/sanity-io/litter v1.2.0 + github.com/sirupsen/logrus v1.4.2 github.com/spf13/cast v1.3.0 - github.com/src-d/go-oniguruma v1.0.0 - github.com/stretchr/testify v1.3.0 - go.etcd.io/bbolt v1.3.2 + github.com/src-d/go-oniguruma v1.1.0 + github.com/stretchr/testify v1.4.0 + go.etcd.io/bbolt v1.3.3 + golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 // indirect + golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 // indirect + golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 // indirect + google.golang.org/appengine v1.6.5 // indirect google.golang.org/grpc v1.25.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 gopkg.in/yaml.v2 v2.2.2 + honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible ) - -replace vitess.io/vitess => ../vitess - -go 1.13 From a213e830efb465bd8329b41f1edc19ff3a8c8289 Mon Sep 17 00:00:00 2001 From: Aaron Son Date: Mon, 2 Dec 2019 10:43:48 -0800 Subject: [PATCH 33/33] Fix identation. --- sql/parse/parse.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sql/parse/parse.go b/sql/parse/parse.go index c9ec912f9e..3a262ff804 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -373,7 +373,7 @@ func convertDDL(ctx *sql.Context, c *sqlparser.DDL) (sql.Node, error) { } return convertCreateTable(c) case sqlparser.DropStr: - if len(c.FromViews) != 0 { + if len(c.FromViews) != 0 { return convertDropView(ctx, c) } return convertDropTable(c) @@ -406,10 +406,10 @@ func convertCreateView(ctx *sql.Context, c *sqlparser.DDL) (sql.Node, error) { return nil, ErrUnsupportedSyntax.New(c.ViewExpr) } - queryNode, err := convertSelect(ctx, selectStatement) - if err != nil { - return nil, err - } + queryNode, err := convertSelect(ctx, selectStatement) + if err != nil { + return nil, err + } queryAlias := plan.NewSubqueryAlias(c.View.Name.String(), queryNode)