From a35189e6ad97f8a96d675b92f8b89f4b86073fa4 Mon Sep 17 00:00:00 2001 From: Kamil Dziedzic Date: Fri, 23 Feb 2018 17:05:06 +0100 Subject: [PATCH] PMM-2087,PMM-2129,PMM-2014,PMM-1928,PMM-1745,PMM-854: Fixes for mixed db + "SELECT dual"; --- app/controllers/query.go | 2 +- app/qan/mysql.go | 21 ++--- app/query/query.go | 36 +++++--- app/query/query_test.go | 14 +++ service/query/mini.go | 181 ++++++++++++++++++------------------- service/query/mini_test.go | 175 +++++++++++++++++++++-------------- 6 files changed, 241 insertions(+), 188 deletions(-) diff --git a/app/controllers/query.go b/app/controllers/query.go index c594a26d..e693c4fa 100644 --- a/app/controllers/query.go +++ b/app/controllers/query.go @@ -63,7 +63,7 @@ func (c *Query) GetTables(id string) revel.Result { } queryHandler := query.NewMySQLHandler(dbm, stats.NullStats()) - tables, _, err := queryHandler.Tables(classId, shared.TableParser) + tables, err := queryHandler.Tables(classId, shared.TableParser) if err != nil { return c.Error(err, "Query.GetTables: queryHandler.Tables") } diff --git a/app/qan/mysql.go b/app/qan/mysql.go index a8c6a148..5af4b9c3 100644 --- a/app/qan/mysql.go +++ b/app/qan/mysql.go @@ -260,7 +260,7 @@ func (h *MySQLMetricWriter) getClassId(checksum string) (uint, error) { } func (h *MySQLMetricWriter) newClass(instanceId uint, subsystem string, class *event.Class, lastSeen string) (uint, error) { - var queryAbstract, queryQuery string + var queryAbstract, queryFingerprint string var tables interface{} switch subsystem { @@ -277,24 +277,24 @@ func (h *MySQLMetricWriter) newClass(instanceId uint, subsystem string, class *e // Truncate long fingerprints and abstracts to avoid MySQL warning 1265: // Data truncated for column 'abstract' - if len(query.Query) > MAX_FINGERPRINT { - query.Query = query.Query[0:MAX_FINGERPRINT-3] + "..." + if len(query.Fingerprint) > MAX_FINGERPRINT { + query.Fingerprint = query.Fingerprint[0:MAX_FINGERPRINT-3] + "..." } if len(query.Abstract) > MAX_ABSTRACT { query.Abstract = query.Abstract[0:MAX_ABSTRACT-3] + "..." } queryAbstract = query.Abstract - queryQuery = query.Query + queryFingerprint = query.Fingerprint case instance.SubsystemNameMongo: queryAbstract = class.Fingerprint - queryQuery = class.Fingerprint + queryFingerprint = class.Fingerprint } // Create the query class which is internally identified by its query_class_id. // The query checksum is the class is identified externally (in a QAN report). // Since this is the first time we've seen the query, firstSeen=lastSeen. t := time.Now() - res, err := h.stmtInsertQueryClass.Exec(class.Id, queryAbstract, queryQuery, tables, lastSeen, lastSeen) + res, err := h.stmtInsertQueryClass.Exec(class.Id, queryAbstract, queryFingerprint, tables, lastSeen, lastSeen) h.stats.TimingDuration(h.stats.System("insert-query-class"), time.Now().Sub(t), h.stats.SampleRate) if err != nil { @@ -327,13 +327,11 @@ func (h *MySQLMetricWriter) getQueryAndTables(class *event.Class) (query.QueryIn return queryInfo, "", fmt.Errorf("empty fingerprint") } - class.Fingerprint = strings.TrimSpace(class.Fingerprint) - // If we have a query example, that's better to parse than a fingerprint - queryExample := class.Fingerprint + queryExample := "" if class.Example != nil && class.Example.Query != "" { queryExample = class.Example.Query } - query, err := h.m.Parse(queryExample, schema) + query, err := h.m.Parse(class.Fingerprint, queryExample, schema) if err != nil { return queryInfo, "", err } @@ -342,9 +340,6 @@ func (h *MySQLMetricWriter) getQueryAndTables(class *event.Class) (query.QueryIn bytes, _ := json.Marshal(query.Tables) tables = string(bytes) } - // We still want to store the fingerprint in the database - // even if an example is available - query.Query = class.Fingerprint return query, tables, nil } diff --git a/app/query/query.go b/app/query/query.go index 503865c7..f4576f35 100644 --- a/app/query/query.go +++ b/app/query/query.go @@ -180,47 +180,59 @@ func (h *MySQLHandler) UpdateTables(classId uint, tables []queryProto.Table) err return nil } -func (h *MySQLHandler) Tables(classId uint, m *queryService.Mini) ([]queryProto.Table, bool, error) { - created := false - +func (h *MySQLHandler) Tables(classId uint, m *queryService.Mini) ([]queryProto.Table, error) { // First try to get the tables. If we're lucky, they've already been parsed // and we're done. var tablesJSON string err := h.dbm.DB().QueryRow("SELECT COALESCE(tables, '') FROM query_classes WHERE query_class_id = ?", classId).Scan(&tablesJSON) if err != nil { - return nil, created, mysql.Error(err, "Tables: SELECT query_classes (tables)") + return nil, mysql.Error(err, "Tables: SELECT query_classes (tables)") } // We're lucky: we already have tables. if tablesJSON != "" { var tables []queryProto.Table if err := json.Unmarshal([]byte(tablesJSON), &tables); err != nil { - return nil, created, err + return nil, err } - return tables, created, nil + return tables, nil } // We're not lucky: this query hasn't been parsed yet, so do it now, if possible. var fingerprint string err = h.dbm.DB().QueryRow("SELECT fingerprint FROM query_classes WHERE query_class_id = ?", classId).Scan(&fingerprint) if err != nil { - return nil, created, mysql.Error(err, "Tables: SELECT query_classes (fingerprint)") + return nil, mysql.Error(err, "Tables: SELECT query_classes (fingerprint)") + } + + // Get database from latest example. + var example, db string + err = h.dbm.DB().QueryRow( + "SELECT query, db "+ + " FROM query_examples "+ + " JOIN query_classes c USING (query_class_id)"+ + " JOIN instances i USING (instance_id)"+ + " WHERE query_class_id = ?"+ + " ORDER BY period DESC", + classId, + ).Scan(&example, &db) + if err != nil { + return nil, mysql.Error(err, "Tables: SELECT query_examples (db)") } // If this returns an error, then youtube/vitess/go/sqltypes/sqlparser // doesn't support the query type. - tableInfo, err := m.Parse(fingerprint, "") + tableInfo, err := m.Parse(fingerprint, example, db) if err != nil { - return nil, created, shared.ErrNotImplemented + return nil, shared.ErrNotImplemented } // The sqlparser was able to handle the query, so marshal the tables // into a string and update the tables column so next time we don't // have to parse the query. if err := h.UpdateTables(classId, tableInfo.Tables); err != nil { - return nil, created, err + return nil, err } - created = true - return tableInfo.Tables, created, nil + return tableInfo.Tables, nil } diff --git a/app/query/query_test.go b/app/query/query_test.go index 40c524d4..fde73cfa 100644 --- a/app/query/query_test.go +++ b/app/query/query_test.go @@ -28,6 +28,7 @@ import ( "github.com/percona/qan-api/app/db" "github.com/percona/qan-api/app/query" "github.com/percona/qan-api/config" + queryService "github.com/percona/qan-api/service/query" "github.com/percona/qan-api/stats" "github.com/percona/qan-api/test" testDb "github.com/percona/qan-api/tests/setup/db" @@ -91,3 +92,16 @@ func (s *TestSuite) TestSimple(t *C) { assert.Equal(t, expect, got) } + +func (s *TestSuite) TestTables(t *C) { + m := queryService.NewMini(config.ApiRootDir + "/service/query") + go m.Run() + defer m.Stop() + + s.testDb.LoadDataInfiles(config.TestDir + "/qan/may-2015") + + qh := query.NewMySQLHandler(db.DBManager, s.nullStats) + got, err := qh.Tables(328, m) + assert.NoError(t, err) + assert.Equal(t, []queryProto.Table([]queryProto.Table{{Db: "percona", Table: "cache"}}), got) +} diff --git a/service/query/mini.go b/service/query/mini.go index 0acff5c6..dd4fe790 100644 --- a/service/query/mini.go +++ b/service/query/mini.go @@ -31,9 +31,9 @@ import ( ) type QueryInfo struct { - Query string - Abstract string - Tables []queryProto.Table + Fingerprint string + Abstract string + Tables []queryProto.Table } type parseTry struct { @@ -57,23 +57,21 @@ func (t protoTables) String() string { } const ( - MAX_JOIN_DEPTH = 20 + MAX_JOIN_DEPTH = 100 ) var ( ErrNotSupported = errors.New("SQL parser does not support the query") - ErrMaxJoinDepth = errors.New("recurse to MAX_JOIN_DEPTH") ) type Mini struct { - Debug bool - cwd string - queryIn chan string - miniOut chan string - parseChan chan parseTry - onlyTables bool - stopChan chan struct{} - defaultSchema string + Debug bool + cwd string + queryIn chan string + miniOut chan string + parseChan chan parseTry + onlyTables bool + stopChan chan struct{} } func NewMini(cwd string) *Mini { @@ -143,16 +141,26 @@ func (m *Mini) Run() { } } -func (m *Mini) Parse(query, defaultDb string) (QueryInfo, error) { +func (m *Mini) Parse(fingerprint, example, defaultDb string) (QueryInfo, error) { + fingerprint = strings.TrimSpace(fingerprint) + example = strings.TrimSpace(example) q := QueryInfo{ - Tables: []queryProto.Table{}, + Fingerprint: fingerprint, + Tables: []queryProto.Table{}, } defer func() { q.Abstract = strings.TrimSpace(q.Abstract) }() if m.Debug { - fmt.Printf("\n\nquery: %s\n", query) + fmt.Printf("\n\nexample: %s\n", example) + fmt.Printf("\n\nfingerprint: %s\n", fingerprint) + } + + query := fingerprint + // If we have a query example, that's better to parse than a fingerprint. + if example != "" { + query = example } // Fingerprints replace IN (1, 2) -> in (?+) but "?+" is not valid SQL so @@ -162,8 +170,6 @@ func (m *Mini) Parse(query, defaultDb string) (QueryInfo, error) { // Internal newlines break everything. query = strings.Replace(query, "\n", " ", -1) - q.Query = query - s, err := sqlparser.Parse(query) if err != nil { if m.Debug { @@ -215,68 +221,65 @@ func (m *Mini) parse() { case p := <-m.parseChan: q := p.q crashChan = p.crashChan - switch p.s.(type) { + switch s := p.s.(type) { case *sqlparser.Select: q.Abstract = "SELECT" - s := p.s.(*sqlparser.Select) if m.Debug { fmt.Printf("struct: %#v\n", s) } - for _, t := range s.From { - if err := m.addTable(&q, t, 0); err != nil { - switch err { - case ErrMaxJoinDepth: - fmt.Printf("WARN: %s (%d): %s\n", err, MAX_JOIN_DEPTH, p.query) - q, _ = m.usePerl(p.query, q, ErrNotSupported) - default: - fmt.Printf("ERROR: %s: %s\n", err, p.query) - } - } + tables := getTablesFromTableExprs(s.From) + if len(tables) > 0 { + q.Tables = append(q.Tables, tables...) + q.Abstract += " " + tables.String() } case *sqlparser.Insert: // REPLACEs will be recognized by sqlparser as INSERTs and the Action field // will have the real command - s := p.s.(*sqlparser.Insert) q.Abstract = strings.ToUpper(s.Action) if m.Debug { fmt.Printf("struct: %#v\n", s) } table := queryProto.Table{ - Db: firstNonEmpty(s.Table.Qualifier.String(), m.defaultSchema), + Db: s.Table.Qualifier.String(), Table: s.Table.Name.String(), } q.Tables = append(q.Tables, table) q.Abstract += " " + table.String() case *sqlparser.Update: q.Abstract = "UPDATE" - s := p.s.(*sqlparser.Update) if m.Debug { fmt.Printf("struct: %#v\n", s) } - tables := m.getTablesFromStmt(s.TableExprs) - q.Tables = append(q.Tables, tables...) - q.Abstract += " " + tables.String() + tables := getTablesFromTableExprs(s.TableExprs) + if len(tables) > 0 { + q.Tables = append(q.Tables, tables...) + q.Abstract += " " + tables.String() + } case *sqlparser.Delete: q.Abstract = "DELETE" - s := p.s.(*sqlparser.Delete) if m.Debug { fmt.Printf("struct: %#v\n", s) } - tables := m.getTablesFromStmt(s.TableExprs) - q.Tables = append(q.Tables, tables...) - q.Abstract += " " + tables.String() + tables := getTablesFromTableExprs(s.TableExprs) + if len(tables) > 0 { + q.Tables = append(q.Tables, tables...) + q.Abstract += " " + tables.String() + } + case *sqlparser.Use: + q.Abstract = "USE" + case *sqlparser.Show: + sql := sqlparser.NewTrackedBuffer(nil) + s.Format(sql) + q.Abstract = strings.ToUpper(sql.String()) default: if m.Debug { fmt.Printf("unsupported type: %#v\n", p.s) } q, _ = m.usePerl(p.query, q, ErrNotSupported) - switch p.s.(type) { - case *sqlparser.Use: - m.defaultSchema = p.s.(*sqlparser.Use).DBName.String() + switch use := p.s.(type) { case *sqlparser.DDL: - use := p.s.(*sqlparser.DDL) table := queryProto.Table{ - Db: firstNonEmpty(use.NewName.Qualifier.String(), m.defaultSchema), + Db: use.NewName.Qualifier.String(), Table: use.NewName.Name.String(), } q.Tables = append(q.Tables, table) @@ -289,35 +292,6 @@ func (m *Mini) parse() { } } -func (m *Mini) getTablesFromStmt(tes sqlparser.TableExprs) protoTables { - t := []queryProto.Table{} - if len(tes) > 0 { - for _, te := range tes { - if ate, ok := te.(*sqlparser.AliasedTableExpr); ok { - if tn, ok := ate.Expr.(sqlparser.TableName); ok { - tbl := tn.Name.String() - schema := tn.Qualifier.String() - table := queryProto.Table{ - Db: firstNonEmpty(schema, m.defaultSchema), - Table: tbl, - } - t = append(t, table) - } - } - } - } - return t -} - -func firstNonEmpty(vals ...string) string { - for _, val := range vals { - if val != "" { - return val - } - } - return "" -} - func (m *Mini) usePerl(query string, q QueryInfo, originalErr error) (QueryInfo, error) { if m.onlyTables { // Caller wants only tables but we can't get them because sqlparser @@ -330,22 +304,30 @@ func (m *Mini) usePerl(query string, q QueryInfo, originalErr error) (QueryInfo, return q, nil } -func (m *Mini) addTable(q *QueryInfo, t sqlparser.TableExpr, depth uint) error { +func getTablesFromTableExprs(tes sqlparser.TableExprs) (tables protoTables) { + for _, te := range tes { + tables = append(tables, getTablesFromTableExpr(te, 0)...) + } + return tables +} + +func getTablesFromTableExpr(te sqlparser.TableExpr, depth uint) (tables protoTables) { if depth > MAX_JOIN_DEPTH { - return ErrMaxJoinDepth + return nil } depth++ - switch a := t.(type) { + switch a := te.(type) { case *sqlparser.AliasedTableExpr: n := a.Expr.(sqlparser.TableName) db := n.Qualifier.String() - tbl := n.Name.String() - table := queryProto.Table{ - Db: firstNonEmpty(db, m.defaultSchema), - Table: tbl, + tbl := parseTableName(n.Name.String()) + if db != "" || tbl != "" { + table := queryProto.Table{ + Db: db, + Table: tbl, + } + tables = append(tables, table) } - q.Tables = append(q.Tables, table) - q.Abstract += " " + table.String() case *sqlparser.JoinTableExpr: // This case happens for JOIN clauses. It recurses to the bottom // of the tree via the left expressions, then it unwinds. E.g. with @@ -364,16 +346,27 @@ func (m *Mini) addTable(q *QueryInfo, t sqlparser.TableExpr, depth uint) error { // store the right-side values: "b" then "c". Because of this, if // MAX_JOIN_DEPTH is reached, we lose the whole tree because if we take // the existing right-side tables, we'll generate a misleading partial - // list of tables, e.g. "SELECT b c". In this case, the caller falls - // back to usePerl() to get the full, correct abstract (but no tables). - // - // todo: maybe a partial list is better than no list? - if err := m.addTable(q, a.LeftExpr, depth); err != nil { - return err - } - if err := m.addTable(q, a.RightExpr, depth); err != nil { - return err - } + // list of tables, e.g. "SELECT b c". + tables = append(tables, getTablesFromTableExpr(a.LeftExpr, depth)...) + tables = append(tables, getTablesFromTableExpr(a.RightExpr, depth)...) + } + + return tables +} + +func parseTableName(tableName string) string { + // https://dev.mysql.com/doc/refman/5.7/en/select.html#idm140358784149168 + // You are permitted to specify DUAL as a dummy table name in situations where no tables are referenced: + // + // ``` + // mysql> SELECT 1 + 1 FROM DUAL; + // -> 2 + // ``` + // DUAL is purely for the convenience of people who require that all SELECT statements + // should have FROM and possibly other clauses. MySQL may ignore the clauses. + // MySQL does not require FROM DUAL if no tables are referenced. + if tableName == "dual" { + tableName = "" } - return nil + return tableName } diff --git a/service/query/mini_test.go b/service/query/mini_test.go index 7480f3e9..291bfb97 100644 --- a/service/query/mini_test.go +++ b/service/query/mini_test.go @@ -41,79 +41,79 @@ func TestParse(t *testing.T) { examples := []example{ ///////////////////////////////////////////////////////////////////// // SELECT - example{ + { "select c from t where id=?", "SELECT t", - []qp.Table{qp.Table{Db: "", Table: "t"}}, + []qp.Table{{Db: "", Table: "t"}}, }, - example{ // #1 + { // #1 "select c from db.t where id=?", "SELECT db.t", - []qp.Table{qp.Table{Db: "db", Table: "t"}}, + []qp.Table{{Db: "db", Table: "t"}}, }, - example{ // #2 + { // #2 "select c from db.t, t2 where id=?", "SELECT db.t t2", []qp.Table{ - qp.Table{Db: "db", Table: "t"}, - qp.Table{Db: "", Table: "t2"}, + {Db: "db", Table: "t"}, + {Db: "", Table: "t2"}, }, }, - example{ // #3 + { // #3 "SELECT /*!40001 SQL_NO_CACHE */ * FROM `film`", "SELECT film", - []qp.Table{qp.Table{Db: "", Table: "film"}}, + []qp.Table{{Db: "", Table: "film"}}, }, - example{ // #4 + { // #4 "select c from ta join tb on (ta.id=tb.id) where id=?", "SELECT ta tb", []qp.Table{ - qp.Table{Db: "", Table: "ta"}, - qp.Table{Db: "", Table: "tb"}, + {Db: "", Table: "ta"}, + {Db: "", Table: "tb"}, }, }, - example{ // #5 + { // #5 "select c from ta join tb on (ta.id=tb.id) join tc on (1=1) where id=?", "SELECT ta tb tc", []qp.Table{ - qp.Table{Db: "", Table: "ta"}, - qp.Table{Db: "", Table: "tb"}, - qp.Table{Db: "", Table: "tc"}, + {Db: "", Table: "ta"}, + {Db: "", Table: "tb"}, + {Db: "", Table: "tc"}, }, }, ///////////////////////////////////////////////////////////////////// // INSERT - example{ // #6 + { // #6 "INSERT INTO my_table (a,b,c) VALUES (1, 2, 3)", "INSERT my_table", - []qp.Table{qp.Table{Db: "", Table: "my_table"}}, + []qp.Table{{Db: "", Table: "my_table"}}, }, - example{ // #7 + { // #7 "INSERT INTO d.t (a,b,c) VALUES (1, 2, 3)", "INSERT d.t", - []qp.Table{qp.Table{Db: "d", Table: "t"}}, + []qp.Table{{Db: "d", Table: "t"}}, }, ///////////////////////////////////////////////////////////////////// // UPDATE - example{ // #8 + { // #8 "update t set foo=?", "UPDATE t", - []qp.Table{qp.Table{Db: "", Table: "t"}}, + []qp.Table{{Db: "", Table: "t"}}, }, ///////////////////////////////////////////////////////////////////// // DELETE - example{ // #9 + { // #9 "delete from t where id in (?+)", "DELETE t", - []qp.Table{qp.Table{Db: "", Table: "t"}}, + []qp.Table{{Db: "", Table: "t"}}, }, ///////////////////////////////////////////////////////////////////// // Other with partial support - example{ // #10 + { // #10 "show status like ?", "SHOW STATUS", []qp.Table{}, @@ -121,35 +121,35 @@ func TestParse(t *testing.T) { ///////////////////////////////////////////////////////////////////// // Not support by sqlparser, falls back to mini.pl - example{ // #11 + { // #11 "REPLACE INTO my_table (a,b,c) VALUES (1, 2, 3)", "REPLACE my_table", - []qp.Table{qp.Table{Db: "", Table: "my_table"}}, + []qp.Table{{Db: "", Table: "my_table"}}, }, - example{ // #12 + { // #12 "OPTIMIZE TABLE `o2408`.`agent_log`", "OPTIMIZE `o2408`.`agent_log`", []qp.Table{}, }, - example{ // #13 + { // #13 "select c from t1 join t2 using (c) where id=?", "SELECT t1 t2", []qp.Table{ - qp.Table{Db: "", Table: "t1"}, - qp.Table{Db: "", Table: "t2"}, + {Db: "", Table: "t1"}, + {Db: "", Table: "t2"}, }, }, - example{ // #14 + { // #14 "insert into data (?)", "INSERT data", []qp.Table{}, }, - example{ // #15 + { // #15 "call\n pita", "CALL pita", []qp.Table{}, }, - example{ // #16 exceeds MAX_JOIN_DEPTH + { // #16 exceeds MAX_JOIN_DEPTH "select c from a" + " join b on (1=1) join c on (1=1) join d on (1=1) join e on (1=1)" + " join f on (1=1) join g on (1=1) join h on (1=1) join i on (1=1)" + @@ -160,68 +160,77 @@ func TestParse(t *testing.T) { " join z on (1=1)" + " where id=?", "SELECT a b c d e f g h i j k l m n o p q r s t u v w x y z", - []qp.Table{}, + []qp.Table{ + {"", "a"}, + {"", "b"}, {"", "c"}, {"", "d"}, {"", "e"}, + {"", "f"}, {"", "g"}, {"", "h"}, {"", "i"}, + {"", "j"}, {"", "k"}, {"", "l"}, {"", "m"}, + {"", "n"}, {"", "o"}, {"", "p"}, {"", "q"}, + {"", "r"}, {"", "s"}, {"", "t"}, {"", "u"}, + {"", "v"}, {"", "w"}, {"", "x"}, {"", "y"}, + {"", "z"}, + }, }, - example{ // #17 exceeds MAX_JOIN_DEPTH + { // #17 "SELECT DISTINCT c\n FROM sbtest1\nWHERE id\nBETWEEN 1\nAND 100\nORDER BY c\n", "SELECT sbtest1", - []qp.Table{qp.Table{Db: "", Table: "sbtest1"}}, + []qp.Table{{Db: "", Table: "sbtest1"}}, }, - example{ // #18 exceeds MAX_JOIN_DEPTH + { // #18 "SELECT DISTINCT c FROM sbtest2 WHERE id BETWEEN 1 AND 100 ORDER BY c", "SELECT sbtest2", - []qp.Table{qp.Table{Db: "", Table: "sbtest2"}}, + []qp.Table{{Db: "", Table: "sbtest2"}}, }, // Don't remove the ; at the end of the next query. // There was an error in the past where a ; at the end was making the // parser to fail and we want to ensure it works now. - example{ // #19 + { // #19 "SELECT * from `sysbenchtest`.`t6002_0`;", "SELECT sysbenchtest.t6002_0", - []qp.Table{qp.Table{Db: "sysbenchtest", Table: "t6002_0"}}, + []qp.Table{{Db: "sysbenchtest", Table: "t6002_0"}}, }, - example{ // #20 + { // #20 "use zapp", "USE", []qp.Table{}, }, // Schema was set as default from the previous USE - example{ // #21 + { // #21 "SELECT * from `t6003_0`;", - "SELECT zapp.t6003_0", - []qp.Table{qp.Table{Db: "zapp", Table: "t6003_0"}}, + "SELECT t6003_0", + []qp.Table{{Db: "", Table: "t6003_0"}}, }, - example{ // #22 + { // #22 "CREATE TABLE t6004 (id int, a varchar(25)) engine=innodb", "CREATE TABLE t6004", - []qp.Table{qp.Table{Db: "zapp", Table: "t6004"}}, + []qp.Table{{Db: "", Table: "t6004"}}, }, - example{ // #23 + { // #23 "ALTER TABLE sakila.actor ADD COLUMN newcol int", "ALTER TABLE sakila.actor", - []qp.Table{qp.Table{Db: "sakila", Table: "actor"}}, + []qp.Table{{Db: "sakila", Table: "actor"}}, }, // Db & Table are empty because CREATE DATABASE is not yet supported by Vitess.sqlparser - example{ // #24 + { // #24 "CREATE DATABASE percona", "CREATE DATABASE percona", []qp.Table{}, }, - example{ // #25 + { // #25 "create index idx ON percona (f1)", "CREATE index", - []qp.Table{qp.Table{Db: "zapp", Table: "percona"}}, + []qp.Table{{Db: "", Table: "percona"}}, }, - example{ // #26 override the default USE + { // #26 override the default USE "create index idx ON brannigan.percona (f1)", "CREATE index", - []qp.Table{qp.Table{Db: "brannigan", Table: "percona"}}, + []qp.Table{{Db: "brannigan", Table: "percona"}}, }, // PMM-1892. Upgraded Vitess libraries to support this query. // Notice that the query below is not exactly the same reported in the ticket; this // query has `auto_increment` between backticks because it is a reserved MySQL word // but MySQL accepts it anyway as a field name while Vitess doesn't. - example{ + { "SELECT table_schema, table_name, column_name, `auto_increment`, " + "pow(2, CASE data_type WHEN 'tinyint' THEN 7 WHEN 'smallint' " + "THEN 15 WHEN 'mediumint' THEN 23 WHEN 'int' THEN 31 WHEN 'bigint' " + @@ -231,22 +240,52 @@ func TestParse(t *testing.T) { "AND t.auto_increment IS NOT NULL", "SELECT information_schema.tables information_schema.columns", []qp.Table{ - qp.Table{Db: "information_schema", Table: "tables"}, - qp.Table{Db: "information_schema", Table: "columns"}, + {Db: "information_schema", Table: "tables"}, + {Db: "information_schema", Table: "columns"}, }, }, + { // #28 + "SELECT @@`version`", + "SELECT", + []qp.Table{}, + }, } - for i, e := range examples { - q, err := m.Parse(e.query, "") - if err != nil { - t.Errorf("Error in test # %d: %s", i, err) - } - if q.Abstract != e.abstract { - t.Errorf("Test # %d: abstracts are different.\nWant: %s\nGot: %s", i, e.abstract, q.Abstract) - } - if !reflect.DeepEqual(q.Tables, e.tables) { - t.Errorf("Test # %d: tables are different.\nWant: %v\nGot: %v", i, e.tables, q.Tables) + // query.NewMini.Parse() should be safe for parallel parsing. + t.Run("examples", func(t *testing.T) { + for _, defaultDb := range []string{"", "Little Bobby Tables"} { + for i, e := range examples { + t.Run(e.query, func(t *testing.T) { + defaultDb := defaultDb + i := i + tables := make([]qp.Table, len(e.tables)) + copy(tables, e.tables) + e := e + e.tables = tables + + t.Parallel() + q, err := m.Parse(e.query, "", defaultDb) + if err != nil { + t.Errorf("Error in test # %d: %s", i, err) + } + // If there is default db then expect it in tables too. + if defaultDb != "" { + for i := range e.tables { + if e.tables[i].Db == "" { + e.tables[i].Db = defaultDb + } + } + } + + if q.Abstract != e.abstract { + t.Errorf("Test # %d: abstracts are different.\nWant: %s\nGot: %s", i, e.abstract, q.Abstract) + } + if !reflect.DeepEqual(q.Tables, e.tables) { + t.Errorf("Test # %d: tables are different.\nWant: %#v\nGot: %#v", i, e.tables, q.Tables) + } + }) + } } - } + }) + }