diff --git a/go/test/endtoend/vtgate/misc_test.go b/go/test/endtoend/vtgate/misc_test.go index 253b0363490..133e8397d98 100644 --- a/go/test/endtoend/vtgate/misc_test.go +++ b/go/test/endtoend/vtgate/misc_test.go @@ -352,6 +352,9 @@ func TestExplainPassthrough(t *testing.T) { got := fmt.Sprintf("%v", result.Rows) require.Contains(t, got, "SIMPLE") // there is a lot more coming from mysql, // but we are trying to make the test less fragile + + result = exec(t, conn, "explain ks.t1") + require.EqualValues(t, 2, len(result.Rows)) } func TestXXHash(t *testing.T) { diff --git a/go/vt/sqlparser/analyzer.go b/go/vt/sqlparser/analyzer.go index 4d7c9424f6a..b44f5b36b1c 100644 --- a/go/vt/sqlparser/analyzer.go +++ b/go/vt/sqlparser/analyzer.go @@ -84,7 +84,7 @@ func ASTToStatementType(stmt Statement) StatementType { return StmtUse case *OtherRead, *OtherAdmin, *Load: return StmtOther - case *Explain: + case Explain: return StmtExplain case *Begin: return StmtBegin diff --git a/go/vt/sqlparser/ast.go b/go/vt/sqlparser/ast.go index 284f90578b2..d2d7adfd951 100644 --- a/go/vt/sqlparser/ast.go +++ b/go/vt/sqlparser/ast.go @@ -45,14 +45,13 @@ type ( // SelectStatement any SELECT statement. SelectStatement interface { + Statement iSelectStatement() - iStatement() iInsertRows() AddOrder(*Order) SetLimit(*Limit) SetLock(lock Lock) MakeDistinct() - SQLNode } // DDLStatement represents any DDL Statement @@ -86,6 +85,12 @@ type ( SQLNode } + // Explain is an interface that represents the Explain statements + Explain interface { + Statement + iExplain() + } + // AddConstraintDefinition represents a ADD CONSTRAINT alter option AddConstraintDefinition struct { ConstraintDefinition *ConstraintDefinition @@ -505,32 +510,12 @@ type ( Name ColIdent } - // Explain represents an EXPLAIN statement - Explain struct { - Type ExplainType - Statement Statement - } - - // ExplainType is an enum for Explain.Type - ExplainType int8 - // CallProc represents a CALL statement CallProc struct { Name TableName Params Exprs } - // OtherRead represents a DESCRIBE, or EXPLAIN statement. - // It should be used only as an indicator. It does not contain - // the full AST for the statement. - OtherRead struct{} - - // OtherAdmin represents a misc statement that relies on ADMIN privileges, - // such as REPAIR, OPTIMIZE, or TRUNCATE statement. - // It should be used only as an indicator. It does not contain - // the full AST for the statement. - OtherAdmin struct{} - // LockType is an enum for Lock Types LockType int8 @@ -550,6 +535,32 @@ type ( // UnlockTables represents the unlock statement UnlockTables struct{} + + // ExplainType is an enum for ExplainStmt.Type + ExplainType int8 + + // ExplainStmt represents an Explain statement + ExplainStmt struct { + Type ExplainType + Statement Statement + } + + // ExplainTab represents the Explain table + ExplainTab struct { + Table TableName + Wild string + } + + // OtherRead represents a DESCRIBE, or EXPLAIN statement. + // It should be used only as an indicator. It does not contain + // the full AST for the statement. + OtherRead struct{} + + // OtherAdmin represents a misc statement that relies on ADMIN privileges, + // such as REPAIR, OPTIMIZE, or TRUNCATE statement. + // It should be used only as an indicator. It does not contain + // the full AST for the statement. + OtherAdmin struct{} ) func (*Union) iStatement() {} @@ -571,7 +582,6 @@ func (*Rollback) iStatement() {} func (*SRollback) iStatement() {} func (*Savepoint) iStatement() {} func (*Release) iStatement() {} -func (*Explain) iStatement() {} func (*OtherRead) iStatement() {} func (*OtherAdmin) iStatement() {} func (*Select) iSelectStatement() {} @@ -592,6 +602,8 @@ func (*DropView) iStatement() {} func (*TruncateTable) iStatement() {} func (*RenameTable) iStatement() {} func (*CallProc) iStatement() {} +func (*ExplainStmt) iStatement() {} +func (*ExplainTab) iStatement() {} func (*CreateView) iDDLStatement() {} func (*AlterView) iDDLStatement() {} @@ -622,6 +634,9 @@ func (*RenameIndex) iAlterOption() {} func (*Validation) iAlterOption() {} func (TableOptions) iAlterOption() {} +func (*ExplainStmt) iExplain() {} +func (*ExplainTab) iExplain() {} + // IsFullyParsed implements the DDLStatement interface func (*TruncateTable) IsFullyParsed() bool { return true @@ -2486,7 +2501,7 @@ func (node *Release) Format(buf *TrackedBuffer) { } // Format formats the node. -func (node *Explain) Format(buf *TrackedBuffer) { +func (node *ExplainStmt) Format(buf *TrackedBuffer) { format := "" switch node.Type { case EmptyType: // do nothing @@ -2498,6 +2513,14 @@ func (node *Explain) Format(buf *TrackedBuffer) { buf.astPrintf(node, "explain %s%v", format, node.Statement) } +// Format formats the node. +func (node *ExplainTab) Format(buf *TrackedBuffer) { + buf.astPrintf(node, "explain %v", node.Table) + if node.Wild != "" { + buf.astPrintf(node, " %s", node.Wild) + } +} + // Format formats the node. func (node *CallProc) Format(buf *TrackedBuffer) { buf.astPrintf(node, "call %v(%v)", node.Name, node.Params) diff --git a/go/vt/sqlparser/parse_test.go b/go/vt/sqlparser/parse_test.go index ea1ece1d5bf..51a7bd7ed10 100644 --- a/go/vt/sqlparser/parse_test.go +++ b/go/vt/sqlparser/parse_test.go @@ -1534,13 +1534,13 @@ var ( output: "explain select * from t", }, { input: "desc foobar", - output: "otherread", + output: "explain foobar", }, { - input: "explain t1", - output: "otherread", + input: "explain t1", }, { - input: "explain t1 col", - output: "otherread", + input: "explain t1 col", + }, { + input: "explain t1 '%col%'", }, { input: "explain select * from t", }, { diff --git a/go/vt/sqlparser/rewriter.go b/go/vt/sqlparser/rewriter.go index 19f1050b06f..d4073481c10 100644 --- a/go/vt/sqlparser/rewriter.go +++ b/go/vt/sqlparser/rewriter.go @@ -358,8 +358,12 @@ func replaceExistsExprSubquery(newNode, parent SQLNode) { parent.(*ExistsExpr).Subquery = newNode.(*Subquery) } -func replaceExplainStatement(newNode, parent SQLNode) { - parent.(*Explain).Statement = newNode.(Statement) +func replaceExplainStmtStatement(newNode, parent SQLNode) { + parent.(*ExplainStmt).Statement = newNode.(Statement) +} + +func replaceExplainTabTable(newNode, parent SQLNode) { + parent.(*ExplainTab).Table = newNode.(TableName) } type replaceExprsItems int @@ -1279,8 +1283,11 @@ func (a *application) apply(parent, node SQLNode, replacer replacerFunc) { case *ExistsExpr: a.apply(node, n.Subquery, replaceExistsExprSubquery) - case *Explain: - a.apply(node, n.Statement, replaceExplainStatement) + case *ExplainStmt: + a.apply(node, n.Statement, replaceExplainStmtStatement) + + case *ExplainTab: + a.apply(node, n.Table, replaceExplainTabTable) case Exprs: replacer := replaceExprsItems(0) diff --git a/go/vt/sqlparser/sql.go b/go/vt/sqlparser/sql.go index eea7d1bc634..82322dd688f 100644 --- a/go/vt/sqlparser/sql.go +++ b/go/vt/sqlparser/sql.go @@ -8291,25 +8291,25 @@ yydefault: yyDollar = yyS[yypt-1 : yypt+1] //line sql.y:2730 { - yyVAL.str = "" + yyVAL.str = yyDollar[1].colIdent.val } case 513: yyDollar = yyS[yypt-1 : yypt+1] //line sql.y:2734 { - yyVAL.str = "" + yyVAL.str = "'" + string(yyDollar[1].bytes) + "'" } case 514: yyDollar = yyS[yypt-3 : yypt+1] //line sql.y:2740 { - yyVAL.statement = &OtherRead{} + yyVAL.statement = &ExplainTab{Table: yyDollar[2].tableName, Wild: yyDollar[3].str} } case 515: yyDollar = yyS[yypt-3 : yypt+1] //line sql.y:2744 { - yyVAL.statement = &Explain{Type: yyDollar[2].explainType, Statement: yyDollar[3].statement} + yyVAL.statement = &ExplainStmt{Type: yyDollar[2].explainType, Statement: yyDollar[3].statement} } case 516: yyDollar = yyS[yypt-2 : yypt+1] diff --git a/go/vt/sqlparser/sql.y b/go/vt/sqlparser/sql.y index 048a14c17de..91945f86166 100644 --- a/go/vt/sqlparser/sql.y +++ b/go/vt/sqlparser/sql.y @@ -2728,21 +2728,21 @@ wild_opt: } | sql_id { - $$ = "" + $$ = $1.val } | STRING { - $$ = "" + $$ = "'" + string($1) + "'" } - + explain_statement: explain_synonyms table_name wild_opt { - $$ = &OtherRead{} + $$ = &ExplainTab{Table: $2, Wild: $3} } | explain_synonyms explain_format_opt explainable_statement { - $$ = &Explain{Type: $2, Statement: $3} + $$ = &ExplainStmt{Type: $2, Statement: $3} } other_statement: diff --git a/go/vt/vtgate/executor_test.go b/go/vt/vtgate/executor_test.go index a92b107b291..628e51618e8 100644 --- a/go/vt/vtgate/executor_test.go +++ b/go/vt/vtgate/executor_test.go @@ -1035,27 +1035,26 @@ func TestExecutorOther(t *testing.T) { for _, stmt := range stmts { for _, tc := range tcs { - sbc1.ExecCount.Set(0) - sbc2.ExecCount.Set(0) - sbclookup.ExecCount.Set(0) - - _, err := executor.Execute(ctx, "TestExecute", NewSafeSession(&vtgatepb.Session{TargetString: tc.targetStr}), stmt, nil) - if tc.hasNoKeyspaceErr { - assert.Error(t, err, errNoKeyspace) - } else if tc.hasDestinationShardErr { - assert.Errorf(t, err, "Destination can only be a single shard for statement: %s, got: DestinationExactKeyRange(-)", stmt) - } else { - assert.NoError(t, err) - } - - diff := cmp.Diff(tc.wantCnts, cnts{ - Sbc1Cnt: sbc1.ExecCount.Get(), - Sbc2Cnt: sbc2.ExecCount.Get(), - SbcLookupCnt: sbclookup.ExecCount.Get(), + t.Run(fmt.Sprintf("%s-%s", stmt, tc.targetStr), func(t *testing.T) { + sbc1.ExecCount.Set(0) + sbc2.ExecCount.Set(0) + sbclookup.ExecCount.Set(0) + + _, err := executor.Execute(ctx, "TestExecute", NewSafeSession(&vtgatepb.Session{TargetString: tc.targetStr}), stmt, nil) + if tc.hasNoKeyspaceErr { + assert.Error(t, err, errNoKeyspace) + } else if tc.hasDestinationShardErr { + assert.Errorf(t, err, "Destination can only be a single shard for statement: %s", stmt) + } else { + assert.NoError(t, err) + } + + utils.MustMatch(t, tc.wantCnts, cnts{ + Sbc1Cnt: sbc1.ExecCount.Get(), + Sbc2Cnt: sbc2.ExecCount.Get(), + SbcLookupCnt: sbclookup.ExecCount.Get(), + }) }) - if diff != "" { - t.Errorf("stmt: %s\ntc: %+v\n-want,+got:\n%s", stmt, tc, diff) - } } } } diff --git a/go/vt/vtgate/planbuilder/builder.go b/go/vt/vtgate/planbuilder/builder.go index 27122528233..2f1463af163 100644 --- a/go/vt/vtgate/planbuilder/builder.go +++ b/go/vt/vtgate/planbuilder/builder.go @@ -158,15 +158,8 @@ func createInstructionFor(query string, stmt sqlparser.Statement, vschema Contex return buildVSchemaDDLPlan(stmt, vschema) case *sqlparser.Use: return buildUsePlan(stmt, vschema) - case *sqlparser.Explain: - if stmt.Type == sqlparser.VitessType { - innerInstruction, err := createInstructionFor(query, stmt.Statement, vschema) - if err != nil { - return nil, err - } - return buildExplainPlan(innerInstruction) - } - return buildOtherReadAndAdmin(query, vschema) + case sqlparser.Explain: + return buildExplainPlan(stmt, vschema) case *sqlparser.OtherRead, *sqlparser.OtherAdmin: return buildOtherReadAndAdmin(query, vschema) case *sqlparser.Set: diff --git a/go/vt/vtgate/planbuilder/explain.go b/go/vt/vtgate/planbuilder/explain.go index 74d912dddbf..e13b423bf19 100644 --- a/go/vt/vtgate/planbuilder/explain.go +++ b/go/vt/vtgate/planbuilder/explain.go @@ -20,26 +20,53 @@ import ( "strings" "vitess.io/vitess/go/sqltypes" + "vitess.io/vitess/go/vt/key" querypb "vitess.io/vitess/go/vt/proto/query" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/vt/vtgate/engine" ) -func extractQuery(m map[string]interface{}) string { - queryObj, ok := m["Query"] - if !ok { - return "" +// Builds an explain-plan for the given Primitive +func buildExplainPlan(stmt sqlparser.Explain, vschema ContextVSchema) (engine.Primitive, error) { + switch explain := stmt.(type) { + case *sqlparser.ExplainTab: + return explainTabPlan(explain, vschema) + case *sqlparser.ExplainStmt: + if explain.Type == sqlparser.VitessType { + return buildVitessTypePlan(explain, vschema) + } + return buildOtherReadAndAdmin(sqlparser.String(explain), vschema) } - query, ok := queryObj.(string) - if !ok { - return "" + return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "BUG unexpected explain type: %T", stmt) +} + +func explainTabPlan(explain *sqlparser.ExplainTab, vschema ContextVSchema) (engine.Primitive, error) { + table, _, _, _, destination, err := vschema.FindTableOrVindex(explain.Table) + if err != nil { + return nil, err } + explain.Table.Qualifier = sqlparser.NewTableIdent("") - return query + if destination == nil { + destination = key.DestinationAnyShard{} + } + + return &engine.Send{ + Keyspace: table.Keyspace, + TargetDestination: destination, + Query: sqlparser.String(explain), + SingleShardOnly: true, + }, nil } -// Builds an explain-plan for the given Primitive -func buildExplainPlan(input engine.Primitive) (engine.Primitive, error) { - descriptions := treeLines(engine.PrimitiveToPlanDescription(input)) +func buildVitessTypePlan(explain *sqlparser.ExplainStmt, vschema ContextVSchema) (engine.Primitive, error) { + innerInstruction, err := createInstructionFor(sqlparser.String(explain.Statement), explain.Statement, vschema) + if err != nil { + return nil, err + } + descriptions := treeLines(engine.PrimitiveToPlanDescription(innerInstruction)) var rows [][]sqltypes.Value for _, line := range descriptions { @@ -74,6 +101,19 @@ func buildExplainPlan(input engine.Primitive) (engine.Primitive, error) { return engine.NewRowsPrimitive(rows, fields), nil } +func extractQuery(m map[string]interface{}) string { + queryObj, ok := m["Query"] + if !ok { + return "" + } + query, ok := queryObj.(string) + if !ok { + return "" + } + + return query +} + type description struct { header string descr engine.PrimitiveDescription diff --git a/go/vt/vtgate/planbuilder/other_read.go b/go/vt/vtgate/planbuilder/other_read.go index ac9654a9498..aaedecc634f 100644 --- a/go/vt/vtgate/planbuilder/other_read.go +++ b/go/vt/vtgate/planbuilder/other_read.go @@ -35,7 +35,6 @@ func buildOtherReadAndAdmin(sql string, vschema ContextVSchema) (engine.Primitiv Keyspace: keyspace, TargetDestination: destination, Query: sql, //This is original sql query to be passed as the parser can provide partial ddl AST. - IsDML: false, SingleShardOnly: true, }, nil } diff --git a/go/vt/vtgate/planbuilder/testdata/other_read_cases.txt b/go/vt/vtgate/planbuilder/testdata/other_read_cases.txt index d183afb081c..0d52d47f4e8 100644 --- a/go/vt/vtgate/planbuilder/testdata/other_read_cases.txt +++ b/go/vt/vtgate/planbuilder/testdata/other_read_cases.txt @@ -57,7 +57,7 @@ }, "TargetDestination": "AnyShard()", "IsDML": false, - "Query": "describe select * from t", + "Query": "explain select * from t", "SingleShardOnly": true } } @@ -75,7 +75,7 @@ }, "TargetDestination": "AnyShard()", "IsDML": false, - "Query": "desc select * from t", + "Query": "explain select * from t", "SingleShardOnly": true } } diff --git a/go/vt/vttablet/sandboxconn/sandboxconn.go b/go/vt/vttablet/sandboxconn/sandboxconn.go index 4589a749084..b2187b4742c 100644 --- a/go/vt/vttablet/sandboxconn/sandboxconn.go +++ b/go/vt/vttablet/sandboxconn/sandboxconn.go @@ -511,7 +511,7 @@ func (sbc *SandboxConn) getNextResult(stmt sqlparser.Statement) *sqltypes.Result case *sqlparser.Select, *sqlparser.Union, *sqlparser.Show, - *sqlparser.Explain, + sqlparser.Explain, *sqlparser.OtherRead: return SingleRowResult case *sqlparser.Set, diff --git a/go/vt/vttablet/tabletserver/planbuilder/permission.go b/go/vt/vttablet/tabletserver/planbuilder/permission.go index 1bad105b4e7..80949b76c02 100644 --- a/go/vt/vttablet/tabletserver/planbuilder/permission.go +++ b/go/vt/vttablet/tabletserver/planbuilder/permission.go @@ -57,7 +57,7 @@ func BuildPermissions(stmt sqlparser.Statement) []Permission { } case *sqlparser.OtherAdmin, *sqlparser.CallProc, *sqlparser.Begin, *sqlparser.Commit, *sqlparser.Rollback, *sqlparser.Load, *sqlparser.Savepoint, *sqlparser.Release, *sqlparser.SRollback, *sqlparser.Set, *sqlparser.Show, - *sqlparser.OtherRead, *sqlparser.Explain: + *sqlparser.OtherRead, sqlparser.Explain: // no op default: panic(fmt.Errorf("BUG: unexpected statement type: %T", node)) diff --git a/go/vt/vttablet/tabletserver/planbuilder/plan.go b/go/vt/vttablet/tabletserver/planbuilder/plan.go index 71d9758354b..340e7edb014 100644 --- a/go/vt/vttablet/tabletserver/planbuilder/plan.go +++ b/go/vt/vttablet/tabletserver/planbuilder/plan.go @@ -202,7 +202,7 @@ func Build(statement sqlparser.Statement, tables map[string]*schema.Table, isRes plan = &Plan{PlanID: PlanDDL, FullQuery: fullQuery} case *sqlparser.Show: plan, err = analyzeShow(stmt, dbName) - case *sqlparser.OtherRead, *sqlparser.Explain: + case *sqlparser.OtherRead, sqlparser.Explain: plan, err = &Plan{PlanID: PlanOtherRead}, nil case *sqlparser.OtherAdmin: plan, err = &Plan{PlanID: PlanOtherAdmin}, nil @@ -254,7 +254,7 @@ func BuildStreaming(sql string, tables map[string]*schema.Table, isReservedConn return nil, vterrors.New(vtrpcpb.Code_FAILED_PRECONDITION, "select with lock not allowed for streaming") } plan.Table = lookupTable(stmt.From, tables) - case *sqlparser.OtherRead, *sqlparser.Show, *sqlparser.Union, *sqlparser.CallProc: + case *sqlparser.OtherRead, *sqlparser.Show, *sqlparser.Union, *sqlparser.CallProc, sqlparser.Explain: // pass default: return nil, vterrors.Errorf(vtrpcpb.Code_FAILED_PRECONDITION, "'%v' not allowed for streaming", sqlparser.String(stmt)) diff --git a/go/vt/vttablet/tabletserver/planbuilder/testdata/stream_cases.txt b/go/vt/vttablet/tabletserver/planbuilder/testdata/stream_cases.txt index 4724e019b94..16ca07e1907 100644 --- a/go/vt/vttablet/tabletserver/planbuilder/testdata/stream_cases.txt +++ b/go/vt/vttablet/tabletserver/planbuilder/testdata/stream_cases.txt @@ -50,7 +50,7 @@ { "PlanID": "SelectStream", "TableName": "", - "FullQuery": "otherread" + "FullQuery": "explain foo" } # dml diff --git a/go/vt/wrangler/vexec_test.go b/go/vt/wrangler/vexec_test.go index 1ed3ccdc1d1..43dbde69db4 100644 --- a/go/vt/wrangler/vexec_test.go +++ b/go/vt/wrangler/vexec_test.go @@ -378,7 +378,7 @@ func TestVExecValidations(t *testing.T) { { name: "unsupported query", query: "describe _vt.vreplication", - errorString: "query not supported by vexec: otherread", + errorString: "query not supported by vexec: explain _vt.vreplication", }, } for _, bq := range badQueries {