Repository: vitessio/vitess
Pull request #9545 (2022-01-20 19:58:00)

Planner improvements
## Description
Two minor planner improvements - first a query hint that allow users to select the planner to use. Sometimes we want to be able to specify a different planner than the one the vtgate is configured with, and this query hint enables this.

```sql
select /*vt+ PLANNER=gen4 */ * from commerce;
```

Also re-introduces the left-to-right planner which mimics `straight join` on mysql, which was lost during the refactor of gen4 this week.

systay: @frouioui WDYT - should we use this to replace the `GEN4_COMPARE_ONLY_GEN4` annotations?
harshit-gangal: > @frouioui WDYT - should we use this to replace the `GEN4_COMPARE_ONLY_GEN4` annotations?

this would basically mean that set PLANNER=Gen4 in the comment. I think we can do that.
harshit-gangal: looks like we have something already `GEN4_COMPARE_ONLY_V3`, `GEN4_COMPARE_ONLY_GEN4`but those names were not intuitive to use, this looks good.
frouioui: > @frouioui WDYT - should we use this to replace the `GEN4_COMPARE_ONLY_GEN4` annotations?

Yes, that is a good idea!

Patch:
diff --git a/go/vt/sqlparser/ast_funcs.go b/go/vt/sqlparser/ast_funcs.go
index f487df73ca..a5d60f6dca 100644
--- a/go/vt/sqlparser/ast_funcs.go
+++ b/go/vt/sqlparser/ast_funcs.go
@@ -1504,6 +1504,9 @@ func IsAggregation(node SQLNode) bool {
 
 // GetFirstSelect gets the first select statement
 func GetFirstSelect(selStmt SelectStatement) *Select {
+	if selStmt == nil {
+		return nil
+	}
 	switch node := selStmt.(type) {
 	case *Select:
 		return node
diff --git a/go/vt/sqlparser/comments.go b/go/vt/sqlparser/comments.go
index aeabdd5dec..fc5764498d 100644
--- a/go/vt/sqlparser/comments.go
+++ b/go/vt/sqlparser/comments.go
@@ -41,6 +41,8 @@ const (
 	DirectiveAllowScatter = "ALLOW_SCATTER"
 	// DirectiveAllowHashJoin lets the planner use hash join if possible
 	DirectiveAllowHashJoin = "ALLOW_HASH_JOIN"
+	// DirectiveQueryPlanner lets the user specify per query which planner should be used
+	DirectiveQueryPlanner = "PLANNER"
 )
 
 func isNonSpace(r rune) bool {
diff --git a/go/vt/vtgate/planbuilder/builder.go b/go/vt/vtgate/planbuilder/builder.go
index 5f843d5fc0..727cb8479e 100644
--- a/go/vt/vtgate/planbuilder/builder.go
+++ b/go/vt/vtgate/planbuilder/builder.go
@@ -20,6 +20,8 @@ import (
 	"errors"
 	"sort"
 
+	"vitess.io/vitess/go/vt/log"
+
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 
 	"vitess.io/vitess/go/sqltypes"
@@ -92,22 +94,51 @@ func BuildFromStmt(query string, stmt sqlparser.Statement, reservedVars *sqlpars
 	return plan, nil
 }
 
-func getConfiguredPlanner(vschema plancontext.VSchema, v3planner selectPlanner) (selectPlanner, error) {
-	switch vschema.Planner() {
+func getConfiguredPlanner(vschema plancontext.VSchema, v3planner func(string) selectPlanner, stmt sqlparser.SelectStatement, query string) (selectPlanner, error) {
+	planner, ok := getPlannerFromQuery(stmt)
+	if !ok {
+		// if the query doesn't specify the planner, we check what the configuration is
+		planner = vschema.Planner()
+	}
+	switch planner {
 	case Gen4CompareV3:
-		return gen4CompareV3Planner, nil
+		return gen4CompareV3Planner(query), nil
 	case Gen4, Gen4Left2Right, Gen4GreedyOnly:
-		return gen4Planner, nil
+		return gen4Planner(query, planner), nil
 	case Gen4WithFallback:
 		fp := &fallbackPlanner{
-			primary:  gen4Planner,
-			fallback: v3planner,
+			primary:  gen4Planner(query, querypb.ExecuteOptions_Gen4),
+			fallback: v3planner(query),
 		}
 		return fp.plan, nil
 	default:
 		// default is v3 plan
-		return v3planner, nil
+		return v3planner(query), nil
+	}
+}
+
+func getPlannerFromQuery(stmt sqlparser.SelectStatement) (plancontext.PlannerVersion, bool) {
+	var d sqlparser.CommentDirectives
+
+	firstSelect := sqlparser.GetFirstSelect(stmt)
+	if firstSelect != nil {
+		d = sqlparser.ExtractCommentDirectives(firstSelect.Comments)
+	}
+	if d == nil {
+		return plancontext.PlannerVersion(0), false
+	}
+
+	val, ok := d[sqlparser.DirectiveQueryPlanner]
+	if !ok {
+		return plancontext.PlannerVersion(0), false
+	}
+
+	str, ok := val.(string)
+	if !ok {
+		log.Errorf("planner specified with unknown type %v", val)
+		return plancontext.PlannerVersion(0), false
 	}
+	return plancontext.PlannerNameToVersion(str)
 }
 
 func buildRoutePlan(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema, f func(statement sqlparser.Statement, reservedVars *sqlparser.ReservedVars, schema plancontext.VSchema) (engine.Primitive, error)) (engine.Primitive, error) {
@@ -117,16 +148,16 @@ func buildRoutePlan(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVa
 	return f(stmt, reservedVars, vschema)
 }
 
-type selectPlanner func(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error)
+type selectPlanner func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error)
 
 func createInstructionFor(query string, stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema, enableOnlineDDL, enableDirectDDL bool) (engine.Primitive, error) {
 	switch stmt := stmt.(type) {
 	case *sqlparser.Select:
-		configuredPlanner, err := getConfiguredPlanner(vschema, buildSelectPlan)
+		configuredPlanner, err := getConfiguredPlanner(vschema, buildSelectPlan, stmt, query)
 		if err != nil {
 			return nil, err
 		}
-		return buildRoutePlan(stmt, reservedVars, vschema, configuredPlanner(query))
+		return buildRoutePlan(stmt, reservedVars, vschema, configuredPlanner)
 	case *sqlparser.Insert:
 		return buildRoutePlan(stmt, reservedVars, vschema, buildInsertPlan)
 	case *sqlparser.Update:
@@ -134,11 +165,11 @@ func createInstructionFor(query string, stmt sqlparser.Statement, reservedVars *
 	case *sqlparser.Delete:
 		return buildRoutePlan(stmt, reservedVars, vschema, buildDeletePlan)
 	case *sqlparser.Union:
-		configuredPlanner, err := getConfiguredPlanner(vschema, buildUnionPlan)
+		configuredPlanner, err := getConfiguredPlanner(vschema, buildUnionPlan, stmt, query)
 		if err != nil {
 			return nil, err
 		}
-		return buildRoutePlan(stmt, reservedVars, vschema, configuredPlanner(query))
+		return buildRoutePlan(stmt, reservedVars, vschema, configuredPlanner)
 	case sqlparser.DDLStatement:
 		return buildGeneralDDLPlan(query, stmt, reservedVars, vschema, enableOnlineDDL, enableDirectDDL)
 	case *sqlparser.AlterMigration:
diff --git a/go/vt/vtgate/planbuilder/fallback_planner.go b/go/vt/vtgate/planbuilder/fallback_planner.go
index c20f159c74..d931322789 100644
--- a/go/vt/vtgate/planbuilder/fallback_planner.go
+++ b/go/vt/vtgate/planbuilder/fallback_planner.go
@@ -31,29 +31,21 @@ type fallbackPlanner struct {
 
 var _ selectPlanner = (*fallbackPlanner)(nil).plan
 
-func (fp *fallbackPlanner) safePrimary(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
-	primaryF := fp.primary(query)
-	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (res engine.Primitive, err error) {
-		defer func() {
-			// if the primary planner panics, we want to catch it here so we can fall back
-			if r := recover(); r != nil {
-				err = fmt.Errorf("%v", r) // not using vterror since this will only be used for logging
-			}
-		}()
-		res, err = primaryF(stmt, reservedVars, vschema)
-		return
-	}
+func (fp *fallbackPlanner) safePrimary(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (res engine.Primitive, err error) {
+	defer func() {
+		// if the primary planner panics, we want to catch it here so we can fall back
+		if r := recover(); r != nil {
+			err = fmt.Errorf("%v", r) // not using vterror since this will only be used for logging
+		}
+	}()
+	res, err = fp.primary(stmt, reservedVars, vschema)
+	return
 }
 
-func (fp *fallbackPlanner) plan(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
-	primaryF := fp.safePrimary(query)
-	backupF := fp.fallback(query)
-
-	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
-		res, err := primaryF(sqlparser.CloneStatement(stmt), reservedVars, vschema)
-		if err != nil {
-			return backupF(stmt, reservedVars, vschema)
-		}
-		return res, nil
+func (fp *fallbackPlanner) plan(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
+	res, err := fp.safePrimary(sqlparser.CloneStatement(stmt), reservedVars, vschema)
+	if err != nil {
+		return fp.fallback(stmt, reservedVars, vschema)
 	}
+	return res, nil
 }
diff --git a/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go b/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go
index ff7f1b143c..364b0603f0 100644
--- a/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go
+++ b/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go
@@ -17,18 +17,12 @@ limitations under the License.
 package planbuilder
 
 import (
-	"strings"
-
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
 
-type commentDirective struct {
-	onlyV3, onlyGen4 bool
-}
-
 func gen4CompareV3Planner(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
 	return func(statement sqlparser.Statement, vars *sqlparser.ReservedVars, ctxVSchema plancontext.VSchema) (engine.Primitive, error) {
 		// we will be switching the planner version to Gen4 and V3 in order to
@@ -37,11 +31,10 @@ func gen4CompareV3Planner(query string) func(sqlparser.Statement, *sqlparser.Res
 		defer ctxVSchema.SetPlannerVersion(Gen4CompareV3)
 
 		// preliminary checks on the given statement
-		onlyGen4, hasOrderBy, comments, err := preliminaryChecks(statement)
+		onlyGen4, hasOrderBy, err := preliminaryChecks(statement)
 		if err != nil {
 			return nil, err
 		}
-		cd := parseComments(comments)
 
 		// plan statement using Gen4
 		gen4Primitive, gen4Err := planWithPlannerVersion(statement, vars, ctxVSchema, query, Gen4)
@@ -51,17 +44,13 @@ func gen4CompareV3Planner(query string) func(sqlparser.Statement, *sqlparser.Res
 		// since lock primitives can imply the creation or deletion of locks,
 		// we want to execute them once using Gen4 to avoid the duplicated locks
 		// or double lock-releases.
-		if !cd.onlyV3 && (onlyGen4 || cd.onlyGen4) || (gen4Primitive != nil && hasLockPrimitive(gen4Primitive)) {
+		if onlyGen4 || (gen4Primitive != nil && hasLockPrimitive(gen4Primitive)) {
 			return gen4Primitive, gen4Err
 		}
 
 		// get V3's plan
 		v3Primitive, v3Err := planWithPlannerVersion(statement, vars, ctxVSchema, query, V3)
 
-		if cd.onlyV3 && !cd.onlyGen4 && !onlyGen4 {
-			return v3Primitive, v3Err
-		}
-
 		// check potential errors from Gen4 and V3
 		err = engine.CompareV3AndGen4Errors(v3Err, gen4Err)
 		if err != nil {
@@ -76,26 +65,11 @@ func gen4CompareV3Planner(query string) func(sqlparser.Statement, *sqlparser.Res
 	}
 }
 
-func parseComments(comments []string) commentDirective {
-	cd := commentDirective{}
-	for _, comment := range comments {
-		if strings.Contains(comment, "GEN4_COMPARE_ONLY_V3") {
-			cd.onlyV3 = true
-		}
-		if strings.Contains(comment, "GEN4_COMPARE_ONLY_GEN4") {
-			cd.onlyGen4 = true
-		}
-	}
-	return cd
-}
-
-func preliminaryChecks(statement sqlparser.Statement) (bool, bool, []string, error) {
+func preliminaryChecks(statement sqlparser.Statement) (bool, bool, error) {
 	var onlyGen4, hasOrderBy bool
-	var comments []string
 	switch s := statement.(type) {
 	case *sqlparser.Union:
 		hasOrderBy = len(s.OrderBy) > 0
-		comments = s.GetComments()
 
 		// walk through the union and search for select statements that have
 		// a next val select expression, in which case we need to only use
@@ -109,11 +83,10 @@ func preliminaryChecks(statement sqlparser.Statement) (bool, bool, []string, err
 			return true, nil
 		}, s)
 		if err != nil {
-			return false, false, nil, err
+			return false, false, err
 		}
 	case *sqlparser.Select:
 		hasOrderBy = len(s.OrderBy) > 0
-		comments = s.GetComments()
 
 		for _, expr := range s.SelectExprs {
 			// we are not executing the plan a second time if the query is a select next val,
@@ -125,7 +98,7 @@ func preliminaryChecks(statement sqlparser.Statement) (bool, bool, []string, err
 			}
 		}
 	}
-	return onlyGen4, hasOrderBy, comments, nil
+	return onlyGen4, hasOrderBy, nil
 }
 
 func planWithPlannerVersion(statement sqlparser.Statement, vars *sqlparser.ReservedVars, ctxVSchema plancontext.VSchema, query string, version plancontext.PlannerVersion) (engine.Primitive, error) {
diff --git a/go/vt/vtgate/planbuilder/gen4_planner.go b/go/vt/vtgate/planbuilder/gen4_planner.go
index f9508006d0..f53da5151d 100644
--- a/go/vt/vtgate/planbuilder/gen4_planner.go
+++ b/go/vt/vtgate/planbuilder/gen4_planner.go
@@ -17,6 +17,7 @@ limitations under the License.
 package planbuilder
 
 import (
+	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"
@@ -27,9 +28,9 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
-var _ selectPlanner = gen4Planner
+var _ selectPlanner = gen4Planner("apa", 0)
 
-func gen4Planner(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
+func gen4Planner(query string, plannerVersion querypb.ExecuteOptions_PlannerVersion) selectPlanner {
 	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
 		selStatement, ok := stmt.(sqlparser.SelectStatement)
 		if !ok {
@@ -62,7 +63,7 @@ func gen4Planner(query string) func(sqlparser.Statement, *sqlparser.ReservedVars
 		}
 
 		getPlan := func(selStatement sqlparser.SelectStatement) (logicalPlan, error) {
-			return newBuildSelectPlan(selStatement, reservedVars, vschema)
+			return newBuildSelectPlan(selStatement, reservedVars, vschema, plannerVersion)
 		}
 
 		plan, err := getPlan(selStatement)
@@ -105,7 +106,7 @@ func gen4planSQLCalcFoundRows(vschema plancontext.VSchema, sel *sqlparser.Select
 }
 
 func planSelectGen4(reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema, sel *sqlparser.Select) (*jointab, logicalPlan, error) {
-	plan, err := newBuildSelectPlan(sel, reservedVars, vschema)
+	plan, err := newBuildSelectPlan(sel, reservedVars, vschema, 0)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -126,7 +127,12 @@ func gen4CNFRewrite(stmt sqlparser.Statement, getPlan func(selStatement sqlparse
 	return nil
 }
 
-func newBuildSelectPlan(selStmt sqlparser.SelectStatement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (logicalPlan, error) {
+func newBuildSelectPlan(
+	selStmt sqlparser.SelectStatement,
+	reservedVars *sqlparser.ReservedVars,
+	vschema plancontext.VSchema,
+	version querypb.ExecuteOptions_PlannerVersion,
+) (logicalPlan, error) {
 	ksName := ""
 	if ks, _ := vschema.DefaultKeyspace(); ks != nil {
 		ksName = ks.Name
@@ -147,7 +153,7 @@ func newBuildSelectPlan(selStmt sqlparser.SelectStatement, reservedVars *sqlpars
 		return nil, err
 	}
 
-	ctx := plancontext.NewPlanningContext(reservedVars, semTable, vschema)
+	ctx := plancontext.NewPlanningContext(reservedVars, semTable, vschema, version)
 	logical, err := abstract.CreateOperatorFromAST(selStmt, semTable)
 	if err != nil {
 		return nil, err
diff --git a/go/vt/vtgate/planbuilder/physical/route_planning.go b/go/vt/vtgate/planbuilder/physical/route_planning.go
index 4a9058d8dd..35b5ee9176 100644
--- a/go/vt/vtgate/planbuilder/physical/route_planning.go
+++ b/go/vt/vtgate/planbuilder/physical/route_planning.go
@@ -21,6 +21,8 @@ import (
 	"fmt"
 	"io"
 
+	querypb "vitess.io/vitess/go/vt/proto/query"
+
 	"vitess.io/vitess/go/mysql/collations"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
@@ -46,8 +48,8 @@ func CreatePhysicalOperator(ctx *plancontext.PlanningContext, opTree abstract.Lo
 	switch op := opTree.(type) {
 	case *abstract.QueryGraph:
 		switch {
-		// case ctx.vschema.Planner() == Gen4Left2Right:
-		//	return leftToRightSolve(ctx, op)
+		case ctx.PlannerVersion == querypb.ExecuteOptions_Gen4Left2Right:
+			return leftToRightSolve(ctx, op)
 		default:
 			return greedySolve(ctx, op)
 		}
@@ -112,6 +114,28 @@ func greedySolve(ctx *plancontext.PlanningContext, qg *abstract.QueryGraph) (abs
 	return op, nil
 }
 
+func leftToRightSolve(ctx *plancontext.PlanningContext, qg *abstract.QueryGraph) (abstract.PhysicalOperator, error) {
+	plans, err := seedOperatorList(ctx, qg)
+	if err != nil {
+		return nil, err
+	}
+
+	var acc abstract.PhysicalOperator
+	for _, plan := range plans {
+		if acc == nil {
+			acc = plan
+			continue
+		}
+		joinPredicates := qg.GetPredicates(acc.TableID(), plan.TableID())
+		acc, err = mergeOrJoin(ctx, acc, plan, joinPredicates, true)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return acc, nil
+}
+
 // seedOperatorList returns a route for each table in the qg
 func seedOperatorList(ctx *plancontext.PlanningContext, qg *abstract.QueryGraph) ([]abstract.PhysicalOperator, error) {
 	plans := make([]abstract.PhysicalOperator, len(qg.Tables))
diff --git a/go/vt/vtgate/planbuilder/plancontext/planning_context.go b/go/vt/vtgate/planbuilder/plancontext/planning_context.go
index 9cd3197ea0..c70f39f86a 100644
--- a/go/vt/vtgate/planbuilder/plancontext/planning_context.go
+++ b/go/vt/vtgate/planbuilder/plancontext/planning_context.go
@@ -17,6 +17,7 @@ limitations under the License.
 package plancontext
 
 import (
+	querypb "vitess.io/vitess/go/vt/proto/query"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
@@ -32,15 +33,17 @@ type PlanningContext struct {
 	// map below
 	JoinPredicates map[sqlparser.Expr][]sqlparser.Expr
 	SkipPredicates map[sqlparser.Expr]interface{}
+	PlannerVersion querypb.ExecuteOptions_PlannerVersion
 }
 
-func NewPlanningContext(reservedVars *sqlparser.ReservedVars, semTable *semantics.SemTable, vschema VSchema) *PlanningContext {
+func NewPlanningContext(reservedVars *sqlparser.ReservedVars, semTable *semantics.SemTable, vschema VSchema, version querypb.ExecuteOptions_PlannerVersion) *PlanningContext {
 	ctx := &PlanningContext{
 		ReservedVars:   reservedVars,
 		SemTable:       semTable,
 		VSchema:        vschema,
 		JoinPredicates: map[sqlparser.Expr][]sqlparser.Expr{},
 		SkipPredicates: map[sqlparser.Expr]interface{}{},
+		PlannerVersion: version,
 	}
 	return ctx
 }
diff --git a/go/vt/vtgate/planbuilder/plancontext/vschema.go b/go/vt/vtgate/planbuilder/plancontext/vschema.go
index ec2303d04e..221a006704 100644
--- a/go/vt/vtgate/planbuilder/plancontext/vschema.go
+++ b/go/vt/vtgate/planbuilder/plancontext/vschema.go
@@ -1,6 +1,8 @@
 package plancontext
 
 import (
+	"strings"
+
 	"vitess.io/vitess/go/mysql/collations"
 	"vitess.io/vitess/go/vt/key"
 	querypb "vitess.io/vitess/go/vt/proto/query"
@@ -48,3 +50,22 @@ type VSchema interface {
 	// ForeignKeyMode returns the foreign_key flag value
 	ForeignKeyMode() string
 }
+
+// PlannerNameToVersion returns the numerical representation of the planner
+func PlannerNameToVersion(s string) (PlannerVersion, bool) {
+	switch strings.ToLower(s) {
+	case "v3":
+		return querypb.ExecuteOptions_V3, true
+	case "gen4":
+		return querypb.ExecuteOptions_Gen4, true
+	case "gen4greedy", "greedy":
+		return querypb.ExecuteOptions_Gen4Greedy, true
+	case "left2right":
+		return querypb.ExecuteOptions_Gen4Left2Right, true
+	case "gen4fallback":
+		return querypb.ExecuteOptions_Gen4WithFallback, true
+	case "gen4comparev3":
+		return querypb.ExecuteOptions_Gen4CompareV3, true
+	}
+	return 0, false
+}
diff --git a/go/vt/vtgate/planbuilder/select.go b/go/vt/vtgate/planbuilder/select.go
index 47bd5d7bec..0462967699 100644
--- a/go/vt/vtgate/planbuilder/select.go
+++ b/go/vt/vtgate/planbuilder/select.go
@@ -35,7 +35,7 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
 
-func buildSelectPlan(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
+func buildSelectPlan(query string) selectPlanner {
 	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
 		sel := stmt.(*sqlparser.Select)
 		if sel.With != nil {
diff --git a/go/vt/vtgate/planbuilder/union.go b/go/vt/vtgate/planbuilder/union.go
index d3e948e262..c0305cdbc4 100644
--- a/go/vt/vtgate/planbuilder/union.go
+++ b/go/vt/vtgate/planbuilder/union.go
@@ -31,7 +31,7 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
 
-func buildUnionPlan(string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
+func buildUnionPlan(string) selectPlanner {
 	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
 		union := stmt.(*sqlparser.Union)
 		if union.With != nil {
diff --git a/go/vt/vtgate/vcursor_impl.go b/go/vt/vtgate/vcursor_impl.go
index d624979657..cba32145a8 100644
--- a/go/vt/vtgate/vcursor_impl.go
+++ b/go/vt/vtgate/vcursor_impl.go
@@ -367,19 +367,9 @@ func (vc *vcursorImpl) Planner() plancontext.PlannerVersion {
 		vc.safeSession.Options.PlannerVersion != querypb.ExecuteOptions_DEFAULT_PLANNER {
 		return vc.safeSession.Options.PlannerVersion
 	}
-	switch strings.ToLower(*plannerVersion) {
-	case "v3":
-		return planbuilder.V3
-	case "gen4":
-		return planbuilder.Gen4
-	case "gen4greedy", "greedy":
-		return planbuilder.Gen4GreedyOnly
-	case "left2right":
-		return planbuilder.Gen4Left2Right
-	case "gen4fallback":
-		return planbuilder.Gen4WithFallback
-	case "gen4comparev3":
-		return planbuilder.Gen4CompareV3
+	version, done := plancontext.PlannerNameToVersion(*plannerVersion)
+	if done {
+		return version
 	}
 
 	log.Warning("unknown planner version configured. using the default")


Test patch:
diff --git a/go/test/endtoend/vtgate/misc_test.go b/go/test/endtoend/vtgate/misc_test.go
index d5426009e0..8b37d39706 100644
--- a/go/test/endtoend/vtgate/misc_test.go
+++ b/go/test/endtoend/vtgate/misc_test.go
@@ -571,10 +571,10 @@ func TestSQLSelectLimit(t *testing.T) {
 		*/
 
 		//	without order by the results are not deterministic for testing purpose. Checking row count only.
-		qr := utils.Exec(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ uid, msg from t7_xxhash union all select uid, msg from t7_xxhash")
+		qr := utils.Exec(t, conn, "select /*vt+ PLANNER=gen4 */ uid, msg from t7_xxhash union all select uid, msg from t7_xxhash")
 		assert.Equal(t, 2, len(qr.Rows))
 
-		qr = utils.Exec(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ uid, msg from t7_xxhash union all select uid, msg from t7_xxhash limit 3")
+		qr = utils.Exec(t, conn, "select /*vt+ PLANNER=gen4 */ uid, msg from t7_xxhash union all select uid, msg from t7_xxhash limit 3")
 		assert.Equal(t, 3, len(qr.Rows))
 	}
 }
@@ -650,7 +650,7 @@ func TestUnionWithManyInfSchemaQueries(t *testing.T) {
 	require.NoError(t, err)
 	defer conn.Close()
 
-	utils.Exec(t, conn, `SELECT /* GEN4_COMPARE_ONLY_GEN4 */ 
+	utils.Exec(t, conn, `SELECT /*vt+ PLANNER=gen4 */ 
                     TABLE_SCHEMA,
                     TABLE_NAME
                 FROM
@@ -802,6 +802,6 @@ func TestFilterAfterLeftJoin(t *testing.T) {
 	utils.Exec(t, conn, "insert into t1 (id1,id2) values (2, 3)")
 	utils.Exec(t, conn, "insert into t1 (id1,id2) values (3, 2)")
 
-	query := "select /* GEN4_COMPARE_ONLY_GEN4 */ A.id1, A.id2 from t1 as A left join t1 as B on A.id1 = B.id2 WHERE B.id1 IS NULL"
+	query := "select /*vt+ PLANNER=gen4 */ A.id1, A.id2 from t1 as A left join t1 as B on A.id1 = B.id2 WHERE B.id1 IS NULL"
 	utils.AssertMatches(t, conn, query, `[[INT64(1) INT64(10)]]`)
 }
diff --git a/go/test/endtoend/vtgate/mysql80/derived/derived_test.go b/go/test/endtoend/vtgate/mysql80/derived/derived_test.go
index 0b6acdc338..42d9cab00c 100644
--- a/go/test/endtoend/vtgate/mysql80/derived/derived_test.go
+++ b/go/test/endtoend/vtgate/mysql80/derived/derived_test.go
@@ -38,5 +38,5 @@ func TestDerivedTableColumns(t *testing.T) {
 	defer utils.Exec(t, conn, `delete from t1`)
 
 	utils.Exec(t, conn, "insert into t1(id1, id2) values (0,10),(1,9),(2,8),(3,7),(4,6),(5,5)")
-	utils.AssertMatches(t, conn, `SELECT /* GEN4_COMPARE_ONLY_GEN4 */ t.id FROM (SELECT id2 FROM t1) AS t(id) ORDER BY t.id DESC`, `[[INT64(10)] [INT64(9)] [INT64(8)] [INT64(7)] [INT64(6)] [INT64(5)]]`)
+	utils.AssertMatches(t, conn, `SELECT /*vt+ PLANNER=gen4 */ t.id FROM (SELECT id2 FROM t1) AS t(id) ORDER BY t.id DESC`, `[[INT64(10)] [INT64(9)] [INT64(8)] [INT64(7)] [INT64(6)] [INT64(5)]]`)
 }
diff --git a/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go b/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
index 9c4d465b47..84df1d2275 100644
--- a/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
+++ b/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
@@ -106,19 +106,19 @@ func TestEqualFilterOnScatter(t *testing.T) {
 		t.Run(workload, func(t *testing.T) {
 			utils.Exec(t, conn, fmt.Sprintf("set workload = '%s'", workload))
 
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 1 = 1", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = 5", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 5 = a", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = a", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = 3+2", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 1+4 = 3+2", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = 1", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = \"1\"", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = \"5\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a = 5.00", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a, val1 from aggr_test group by val1 having a = 1.00", `[[INT64(1) VARCHAR("a")] [INT64(1) VARCHAR("b")] [INT64(1) VARCHAR("c")] [INT64(1) VARCHAR("d")] [INT64(1) VARCHAR("e")]]`)
-
-			utils.AssertContainsError(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ 1 from aggr_test having count(*) = 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 1 = 1", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = 5", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 5 = a", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = a", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = 3+2", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 1+4 = 3+2", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = 1", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = \"1\"", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = \"5\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = 5.00", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a, val1 from aggr_test group by val1 having a = 1.00", `[[INT64(1) VARCHAR("a")] [INT64(1) VARCHAR("b")] [INT64(1) VARCHAR("c")] [INT64(1) VARCHAR("d")] [INT64(1) VARCHAR("e")]]`)
+
+			utils.AssertContainsError(t, conn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) = 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
 		})
 	}
 }
@@ -142,16 +142,16 @@ func TestNotEqualFilterOnScatter(t *testing.T) {
 		t.Run(workload, func(t *testing.T) {
 			utils.Exec(t, conn, fmt.Sprintf("set workload = '%s'", workload))
 
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != 5", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 5 != a", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != a", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != 3+2", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != 1", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != \"1\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != \"5\"", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a != 5.00", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != 5", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 5 != a", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != a", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != 3+2", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != 1", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != \"1\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != \"5\"", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != 5.00", `[]`)
 
-			utils.AssertContainsError(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ 1 from aggr_test having count(*) != 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
+			utils.AssertContainsError(t, conn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) != 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
 		})
 	}
 }
@@ -174,16 +174,16 @@ func TestLessFilterOnScatter(t *testing.T) {
 	for _, workload := range workloads {
 		t.Run(workload, func(t *testing.T) {
 			utils.Exec(t, conn, fmt.Sprintf("set workload = '%s'", workload))
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < 10", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 1 < a", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < a", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < 3+2", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < 1", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < \"10\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < \"5\"", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a < 6.00", `[[INT64(5)]]`)
-
-			utils.AssertContainsError(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ 1 from aggr_test having count(*) < 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < 10", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 1 < a", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < a", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < 3+2", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < 1", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < \"10\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < \"5\"", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < 6.00", `[[INT64(5)]]`)
+
+			utils.AssertContainsError(t, conn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) < 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
 		})
 	}
 }
@@ -207,16 +207,16 @@ func TestLessEqualFilterOnScatter(t *testing.T) {
 		t.Run(workload, func(t *testing.T) {
 			utils.Exec(t, conn, fmt.Sprintf("set workload = '%s'", workload))
 
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= 10", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 1 <= a", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= a", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= 3+2", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= 1", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= \"10\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= \"5\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a <= 5.00", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= 10", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 1 <= a", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= a", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= 3+2", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= 1", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= \"10\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= \"5\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= 5.00", `[[INT64(5)]]`)
 
-			utils.AssertContainsError(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ 1 from aggr_test having count(*) <= 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
+			utils.AssertContainsError(t, conn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) <= 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
 		})
 	}
 }
@@ -240,16 +240,16 @@ func TestGreaterFilterOnScatter(t *testing.T) {
 		t.Run(workload, func(t *testing.T) {
 			utils.Exec(t, conn, fmt.Sprintf("set workload = '%s'", workload))
 
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > 1", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 1 > a", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > a", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > 3+1", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > 10", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > \"1\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > \"5\"", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a > 4.00", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > 1", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 1 > a", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > a", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > 3+1", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > 10", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > \"1\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > \"5\"", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > 4.00", `[[INT64(5)]]`)
 
-			utils.AssertContainsError(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ 1 from aggr_test having count(*) > 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
+			utils.AssertContainsError(t, conn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) > 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
 		})
 	}
 }
@@ -273,16 +273,16 @@ func TestGreaterEqualFilterOnScatter(t *testing.T) {
 		t.Run(workload, func(t *testing.T) {
 			utils.Exec(t, conn, fmt.Sprintf("set workload = '%s'", workload))
 
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= 1", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having 1 >= a", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= a", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= 3+2", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= 10", `[]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= \"1\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= \"5\"", `[[INT64(5)]]`)
-			utils.AssertMatches(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ count(*) as a from aggr_test having a >= 5.00", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= 1", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having 1 >= a", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= a", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= 3+2", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= 10", `[]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= \"1\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= \"5\"", `[[INT64(5)]]`)
+			utils.AssertMatches(t, conn, "select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= 5.00", `[[INT64(5)]]`)
 
-			utils.AssertContainsError(t, conn, "select /* GEN4_COMPARE_ONLY_GEN4 */ 1 from aggr_test having count(*) >= 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
+			utils.AssertContainsError(t, conn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) >= 5", `expr cannot be converted, not supported`) // will fail since `count(*)` is a FuncExpr
 		})
 	}
 }
diff --git a/go/test/endtoend/vtgate/queries/informationschema/informationschema_test.go b/go/test/endtoend/vtgate/queries/informationschema/informationschema_test.go
index 53c228b907..9813768495 100644
--- a/go/test/endtoend/vtgate/queries/informationschema/informationschema_test.go
+++ b/go/test/endtoend/vtgate/queries/informationschema/informationschema_test.go
@@ -86,8 +86,8 @@ func TestInformationSchemaQueryGetsRoutedToTheRightTableAndKeyspace(t *testing.T
 
 	utils.Exec(t, conn, "insert into t1(id1, id2) values (1, 1), (2, 2), (3,3), (4,4)")
 
-	_ = utils.Exec(t, conn, "SELECT /* GEN4_COMPARE_ONLY_GEN4 */ * FROM t1000") // test that the routed table is available to us
-	result := utils.Exec(t, conn, "SELECT /* GEN4_COMPARE_ONLY_GEN4 */ * FROM information_schema.tables WHERE table_schema = database() and table_name='t1000'")
+	_ = utils.Exec(t, conn, "SELECT /*vt+ PLANNER=gen4 */ * FROM t1000") // test that the routed table is available to us
+	result := utils.Exec(t, conn, "SELECT /*vt+ PLANNER=gen4 */ * FROM information_schema.tables WHERE table_schema = database() and table_name='t1000'")
 	assert.NotEmpty(t, result.Rows)
 }
 
diff --git a/go/vt/vtgate/planbuilder/fallback_planner_test.go b/go/vt/vtgate/planbuilder/fallback_planner_test.go
index 1743d6d93c..7c45c40ddf 100644
--- a/go/vt/vtgate/planbuilder/fallback_planner_test.go
+++ b/go/vt/vtgate/planbuilder/fallback_planner_test.go
@@ -39,17 +39,15 @@ type testPlanner struct {
 
 var _ selectPlanner = (*testPlanner)(nil).plan
 
-func (tp *testPlanner) plan(_ string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
-	return func(statement sqlparser.Statement, vars *sqlparser.ReservedVars, schema plancontext.VSchema) (engine.Primitive, error) {
-		tp.called = true
-		if tp.panic != nil {
-			panic(tp.panic)
-		}
-		if tp.messWithAST != nil {
-			tp.messWithAST(statement)
-		}
-		return tp.res, tp.err
+func (tp *testPlanner) plan(statement sqlparser.Statement, vars *sqlparser.ReservedVars, schema plancontext.VSchema) (engine.Primitive, error) {
+	tp.called = true
+	if tp.panic != nil {
+		panic(tp.panic)
 	}
+	if tp.messWithAST != nil {
+		tp.messWithAST(statement)
+	}
+	return tp.res, tp.err
 }
 
 func TestFallbackPlanner(t *testing.T) {
@@ -64,14 +62,14 @@ func TestFallbackPlanner(t *testing.T) {
 	var vschema plancontext.VSchema
 
 	// first planner succeeds
-	_, _ = fb.plan("query")(stmt, nil, vschema)
+	_, _ = fb.plan(stmt, nil, vschema)
 	assert.True(t, a.called)
 	assert.False(t, b.called)
 	a.called = false
 
 	// first planner errors
 	a.err = fmt.Errorf("fail")
-	_, _ = fb.plan("query")(stmt, nil, vschema)
+	_, _ = fb.plan(stmt, nil, vschema)
 	assert.True(t, a.called)
 	assert.True(t, b.called)
 
@@ -80,7 +78,7 @@ func TestFallbackPlanner(t *testing.T) {
 
 	// first planner panics
 	a.panic = "oh noes"
-	_, _ = fb.plan("query")(stmt, nil, vschema)
+	_, _ = fb.plan(stmt, nil, vschema)
 	assert.True(t, a.called)
 	assert.True(t, b.called)
 }
@@ -104,7 +102,7 @@ func TestFallbackClonesBeforePlanning(t *testing.T) {
 	var vschema plancontext.VSchema
 
 	// first planner succeeds
-	_, _ = fb.plan("query")(stmt, nil, vschema)
+	_, _ = fb.plan(stmt, nil, vschema)
 
 	assert.NotNilf(t, stmt.SelectExprs, "should not have changed")
 }
diff --git a/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt b/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt
index 6c376ee933..f6f52c098f 100644
--- a/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt
@@ -1427,3 +1427,88 @@
     ]
   }
 }
+
+# derived table with column aliases not supported by v3, but planner is overridden with hint
+"select /*vt+ PLANNER=gen4 */ u.a from (select id as b, name from user) u(a, n) where u.n = 1"
+{
+  "QueryType": "SELECT",
+  "Original": "select /*vt+ PLANNER=gen4 */ u.a from (select id as b, name from user) u(a, n) where u.n = 1",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "Equal",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
+    "Query": "select /*vt+ PLANNER=gen4 */ u.a from (select id as b, `name` from `user` where `name` = 1) as u(a, n)",
+    "Table": "`user`",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "name_user_map"
+  }
+}
+Gen4 plan same as above
+
+# derived table with column aliases not supported by v3, but planner is overridden with hint
+"select /*vt+ PLANNER=v3 */ u.a from (select id as b, name from user) u(a, n) where u.n = 1"
+"unsupported: column aliases in derived table"
+Gen4 plan same as above
+
+# Three-way join using the left2right. The normal gen4 planner would merge m1 and m2 first, but the left to right doesnt
+"select /*vt+ PLANNER=left2right */ user.col from user join unsharded as m1 join unsharded as m2"
+{
+  "QueryType": "SELECT",
+  "Original": "select /*vt+ PLANNER=left2right */ user.col from user join unsharded as m1 join unsharded as m2",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-1",
+    "TableName": "`user`_unsharded_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "Join",
+        "Variant": "Join",
+        "JoinColumnIndexes": "-1",
+        "TableName": "`user`_unsharded",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "Scatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select `user`.col from `user` where 1 != 1",
+            "Query": "select /*vt+ PLANNER=left2right */ `user`.col from `user`",
+            "Table": "`user`"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "Unsharded",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select 1 from unsharded as m1 where 1 != 1",
+            "Query": "select /*vt+ PLANNER=left2right */ 1 from unsharded as m1",
+            "Table": "unsharded"
+          }
+        ]
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "Unsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select 1 from unsharded as m2 where 1 != 1",
+        "Query": "select /*vt+ PLANNER=left2right */ 1 from unsharded as m2",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
+Gen4 plan same as above