Repository: vitessio/vitess
Pull request #10332 (2022-05-18 14:22:58)

[gen4] More aggregation support
## Description
This change makes it possible for the gen4 planner to accept aggregation functions in `HAVING` that are not returned to the user, such as:

```sql
select id 
from tbl 
group by id 
having count(*) > 10
```

## Checklist

-   [x] "Backport me!" label has been added if this change should be backported
-   [x] Tests were added or are not required
-   [x] Documentation was added or is not required


github-actions[bot]: ### Review Checklist

Hello reviewers! :wave: Please follow this checklist when reviewing this Pull Request.

#### General
- [ ] Ensure that the Pull Request has the correct `release notes` label. `release notes none` should only be used for PRs that are so trivial that they need not be included.
- [ ] If a new flag is being introduced, review whether it is really needed. The flag names should be clear and intuitive (as far as possible), and the flag's help should be descriptive.

#### Bug fixes
- [ ] There should be at least one unit or end-to-end test.
- [ ] The Pull Request description should either include a link to an issue that describes the bug OR an actual description of the bug and how to reproduce, along with a description of the fix.

#### Non-trivial changes
- [ ] There should be some code comments as to why things are implemented the way they are.

#### New/Existing features
- [ ] Should be documented, either by modifying the existing documentation or creating new documentation.
- [ ] New features should have a link to a feature request issue or an RFC that documents the use cases, corner cases and test cases.

#### Backward compatibility            
- [ ] Protobuf changes should be wire-compatible.
- [ ] Changes to `_vt` tables and RPCs need to be backward compatible.
- [ ] `vtctl` command output order should be stable and `awk`-able.
frouioui: It would be nice to add an end-to-end test in the `/queries/aggregation` package

Patch:
diff --git a/go/cmd/vtadmin/main.go b/go/cmd/vtadmin/main.go
index 227a395f76..6e2bda475d 100644
--- a/go/cmd/vtadmin/main.go
+++ b/go/cmd/vtadmin/main.go
@@ -171,6 +171,9 @@ func main() {
 	rootCmd.Flags().BoolVar(&httpOpts.DisableDebug, "http-no-debug", false, "whether to disable /debug/pprof/* and /debug/env HTTP endpoints")
 	rootCmd.Flags().Var(&debug.OmitEnv, "http-debug-omit-env", "name of an environment variable to omit from /debug/env, if http debug endpoints are enabled. specify multiple times to omit multiple env vars")
 	rootCmd.Flags().Var(&debug.SanitizeEnv, "http-debug-sanitize-env", "name of an environment variable to sanitize in /debug/env, if http debug endpoints are enabled. specify multiple times to sanitize multiple env vars")
+	rootCmd.Flags().StringVar(&opts.MetricsEndpoint, "http-metrics-endpoint", "/metrics",
+		"HTTP endpoint to expose prometheus metrics on. Omit to disable scraping metrics. "+
+			"Using a path used by VTAdmin's http API is unsupported and causes undefined behavior.")
 	rootCmd.Flags().StringSliceVar(&httpOpts.CORSOrigins, "http-origin", []string{}, "repeated, comma-separated flag of allowed CORS origins. omit to disable CORS")
 	rootCmd.Flags().StringVar(&httpOpts.ExperimentalOptions.TabletURLTmpl,
 		"http-tablet-url-tmpl",
diff --git a/go/vt/schemadiff/table.go b/go/vt/schemadiff/table.go
index 3c7b05194c..18994cfff9 100644
--- a/go/vt/schemadiff/table.go
+++ b/go/vt/schemadiff/table.go
@@ -354,12 +354,10 @@ func (c *CreateTableEntity) normalizeColumnOptions() {
 		// Remove any lengths for integral types since it is deprecated there and
 		// doesn't mean anything anymore.
 		if _, ok := integralTypes[col.Type.Type]; ok {
-			col.Type.Length = nil
-			// Remove zerofill for integral types but keep the behavior that this marks the value
-			// as unsigned
-			if col.Type.Zerofill {
-				col.Type.Zerofill = false
-				col.Type.Unsigned = true
+			// We can remove the length except when we have a boolean, which is
+			// stored as a tinyint(1) and treated special.
+			if !isBool(col.Type) {
+				col.Type.Length = nil
 			}
 		}
 
@@ -403,6 +401,10 @@ func (c *CreateTableEntity) normalizeColumnOptions() {
 	}
 }
 
+func isBool(colType sqlparser.ColumnType) bool {
+	return colType.Type == "tinyint" && colType.Length != nil && sqlparser.CanonicalString(colType.Length) == "1"
+}
+
 func (c *CreateTableEntity) normalizePartitionOptions() {
 	if c.CreateTable.TableSpec.PartitionOption == nil {
 		return
diff --git a/go/vt/sqlparser/ast.go b/go/vt/sqlparser/ast.go
index acd91f5d53..01d61ceaa3 100644
--- a/go/vt/sqlparser/ast.go
+++ b/go/vt/sqlparser/ast.go
@@ -2353,8 +2353,12 @@ type (
 		JSONVal Expr
 	}
 
-	// Offset is another AST type that is used during planning and never produced by the parser
-	Offset int
+	// Offset is an AST type that is used during planning and never produced by the parser
+	// it is the column offset from the incoming result stream
+	Offset struct {
+		V        int
+		Original string
+	}
 
 	// JSONArrayExpr represents JSON_ARRAY()
 	// More information on https://dev.mysql.com/doc/refman/8.0/en/json-creation-functions.html#function_json-array
@@ -2597,7 +2601,7 @@ func (*ExtractedSubquery) iExpr()                  {}
 func (*TrimFuncExpr) iExpr()                       {}
 func (*JSONSchemaValidFuncExpr) iExpr()            {}
 func (*JSONSchemaValidationReportFuncExpr) iExpr() {}
-func (Offset) iExpr()                              {}
+func (*Offset) iExpr()                             {}
 func (*JSONPrettyExpr) iExpr()                     {}
 func (*JSONStorageFreeExpr) iExpr()                {}
 func (*JSONStorageSizeExpr) iExpr()                {}
diff --git a/go/vt/sqlparser/ast_clone.go b/go/vt/sqlparser/ast_clone.go
index c771ab85ee..6b1c85e7c2 100644
--- a/go/vt/sqlparser/ast_clone.go
+++ b/go/vt/sqlparser/ast_clone.go
@@ -255,8 +255,8 @@ func CloneSQLNode(in SQLNode) SQLNode {
 		return CloneRefOfNotExpr(in)
 	case *NullVal:
 		return CloneRefOfNullVal(in)
-	case Offset:
-		return in
+	case *Offset:
+		return CloneRefOfOffset(in)
 	case OnDup:
 		return CloneOnDup(in)
 	case *OptLike:
@@ -1612,6 +1612,15 @@ func CloneRefOfNullVal(n *NullVal) *NullVal {
 	return &out
 }
 
+// CloneRefOfOffset creates a deep clone of the input.
+func CloneRefOfOffset(n *Offset) *Offset {
+	if n == nil {
+		return nil
+	}
+	out := *n
+	return &out
+}
+
 // CloneOnDup creates a deep clone of the input.
 func CloneOnDup(n OnDup) OnDup {
 	if n == nil {
@@ -2815,8 +2824,8 @@ func CloneExpr(in Expr) Expr {
 		return CloneRefOfNotExpr(in)
 	case *NullVal:
 		return CloneRefOfNullVal(in)
-	case Offset:
-		return in
+	case *Offset:
+		return CloneRefOfOffset(in)
 	case *OrExpr:
 		return CloneRefOfOrExpr(in)
 	case *Subquery:
@@ -2961,8 +2970,8 @@ func CloneJSONPathParam(in JSONPathParam) JSONPathParam {
 		return CloneRefOfNotExpr(in)
 	case *NullVal:
 		return CloneRefOfNullVal(in)
-	case Offset:
-		return in
+	case *Offset:
+		return CloneRefOfOffset(in)
 	case *OrExpr:
 		return CloneRefOfOrExpr(in)
 	case *Subquery:
diff --git a/go/vt/sqlparser/ast_equals.go b/go/vt/sqlparser/ast_equals.go
index 41ce6a314d..0d311eb350 100644
--- a/go/vt/sqlparser/ast_equals.go
+++ b/go/vt/sqlparser/ast_equals.go
@@ -722,12 +722,12 @@ func EqualsSQLNode(inA, inB SQLNode) bool {
 			return false
 		}
 		return EqualsRefOfNullVal(a, b)
-	case Offset:
-		b, ok := inB.(Offset)
+	case *Offset:
+		b, ok := inB.(*Offset)
 		if !ok {
 			return false
 		}
-		return a == b
+		return EqualsRefOfOffset(a, b)
 	case OnDup:
 		b, ok := inB.(OnDup)
 		if !ok {
@@ -2607,6 +2607,18 @@ func EqualsRefOfNullVal(a, b *NullVal) bool {
 	return true
 }
 
+// EqualsRefOfOffset does deep equals between the two objects.
+func EqualsRefOfOffset(a, b *Offset) bool {
+	if a == b {
+		return true
+	}
+	if a == nil || b == nil {
+		return false
+	}
+	return a.V == b.V &&
+		a.Original == b.Original
+}
+
 // EqualsOnDup does deep equals between the two objects.
 func EqualsOnDup(a, b OnDup) bool {
 	if len(a) != len(b) {
@@ -4454,12 +4466,12 @@ func EqualsExpr(inA, inB Expr) bool {
 			return false
 		}
 		return EqualsRefOfNullVal(a, b)
-	case Offset:
-		b, ok := inB.(Offset)
+	case *Offset:
+		b, ok := inB.(*Offset)
 		if !ok {
 			return false
 		}
-		return a == b
+		return EqualsRefOfOffset(a, b)
 	case *OrExpr:
 		b, ok := inB.(*OrExpr)
 		if !ok {
@@ -4850,12 +4862,12 @@ func EqualsJSONPathParam(inA, inB JSONPathParam) bool {
 			return false
 		}
 		return EqualsRefOfNullVal(a, b)
-	case Offset:
-		b, ok := inB.(Offset)
+	case *Offset:
+		b, ok := inB.(*Offset)
 		if !ok {
 			return false
 		}
-		return a == b
+		return EqualsRefOfOffset(a, b)
 	case *OrExpr:
 		b, ok := inB.(*OrExpr)
 		if !ok {
diff --git a/go/vt/sqlparser/ast_format.go b/go/vt/sqlparser/ast_format.go
index 7be21d9633..5f8eadf912 100644
--- a/go/vt/sqlparser/ast_format.go
+++ b/go/vt/sqlparser/ast_format.go
@@ -2144,8 +2144,13 @@ func (node *JtOnResponse) Format(buf *TrackedBuffer) {
 }
 
 // Format formats the node.
-func (node Offset) Format(buf *TrackedBuffer) {
-	buf.astPrintf(node, "[%d]", int(node))
+// Using capital letter for this function as an indicator that it's not a normal function call ¯\_(ツ)_/¯
+func (node *Offset) Format(buf *TrackedBuffer) {
+	if node.Original == "" {
+		buf.astPrintf(node, "OFFSET(%d)", node.V)
+	} else {
+		buf.astPrintf(node, "OFFSET(%d, '%s')", node.V, node.Original)
+	}
 }
 
 // Format formats the node.
@@ -2160,7 +2165,7 @@ func (node *JSONSchemaValidationReportFuncExpr) Format(buf *TrackedBuffer) {
 
 // Format formats the node.
 func (node *JSONArrayExpr) Format(buf *TrackedBuffer) {
-	//buf.astPrintf(node,"%s(,"node.Name.Lowered())
+	// buf.astPrintf(node,"%s(,"node.Name.Lowered())
 	buf.literal("json_array(")
 	if len(node.Params) > 0 {
 		var prefix string
@@ -2174,7 +2179,7 @@ func (node *JSONArrayExpr) Format(buf *TrackedBuffer) {
 
 // Format formats the node.
 func (node *JSONObjectExpr) Format(buf *TrackedBuffer) {
-	//buf.astPrintf(node,"%s(,"node.Name.Lowered())
+	// buf.astPrintf(node,"%s(,"node.Name.Lowered())
 	buf.literal("json_object(")
 	if len(node.Params) > 0 {
 		for i, p := range node.Params {
diff --git a/go/vt/sqlparser/ast_format_fast.go b/go/vt/sqlparser/ast_format_fast.go
index d48e056e73..069cfc5bc4 100644
--- a/go/vt/sqlparser/ast_format_fast.go
+++ b/go/vt/sqlparser/ast_format_fast.go
@@ -2793,10 +2793,19 @@ func (node *JtOnResponse) formatFast(buf *TrackedBuffer) {
 }
 
 // formatFast formats the node.
-func (node Offset) formatFast(buf *TrackedBuffer) {
-	buf.WriteByte('[')
-	buf.WriteString(fmt.Sprintf("%d", int(node)))
-	buf.WriteByte(']')
+// Using capital letter for this function as an indicator that it's not a normal function call ¯\_(ツ)_/¯
+func (node *Offset) formatFast(buf *TrackedBuffer) {
+	if node.Original == "" {
+		buf.WriteString("OFFSET(")
+		buf.WriteString(fmt.Sprintf("%d", node.V))
+		buf.WriteByte(')')
+	} else {
+		buf.WriteString("OFFSET(")
+		buf.WriteString(fmt.Sprintf("%d", node.V))
+		buf.WriteString(", '")
+		buf.WriteString(node.Original)
+		buf.WriteString("')")
+	}
 }
 
 // formatFast formats the node.
@@ -2819,7 +2828,7 @@ func (node *JSONSchemaValidationReportFuncExpr) formatFast(buf *TrackedBuffer) {
 
 // formatFast formats the node.
 func (node *JSONArrayExpr) formatFast(buf *TrackedBuffer) {
-	//buf.astPrintf(node,"%s(,"node.Name.Lowered())
+	// buf.astPrintf(node,"%s(,"node.Name.Lowered())
 	buf.WriteString("json_array(")
 	if len(node.Params) > 0 {
 		var prefix string
@@ -2834,7 +2843,7 @@ func (node *JSONArrayExpr) formatFast(buf *TrackedBuffer) {
 
 // formatFast formats the node.
 func (node *JSONObjectExpr) formatFast(buf *TrackedBuffer) {
-	//buf.astPrintf(node,"%s(,"node.Name.Lowered())
+	// buf.astPrintf(node,"%s(,"node.Name.Lowered())
 	buf.WriteString("json_object(")
 	if len(node.Params) > 0 {
 		for i, p := range node.Params {
diff --git a/go/vt/sqlparser/ast_funcs.go b/go/vt/sqlparser/ast_funcs.go
index 966893730f..2a07f97109 100644
--- a/go/vt/sqlparser/ast_funcs.go
+++ b/go/vt/sqlparser/ast_funcs.go
@@ -670,6 +670,10 @@ func NewColIdentWithAt(str string, at AtCount) ColIdent {
 	}
 }
 
+func NewOffset(v int, original Expr) *Offset {
+	return &Offset{V: v, Original: String(original)}
+}
+
 // IsEmpty returns true if the name is empty.
 func (node ColIdent) IsEmpty() bool {
 	return node.val == ""
diff --git a/go/vt/sqlparser/ast_rewrite.go b/go/vt/sqlparser/ast_rewrite.go
index 2efcebfd0d..0d6f2f4264 100644
--- a/go/vt/sqlparser/ast_rewrite.go
+++ b/go/vt/sqlparser/ast_rewrite.go
@@ -254,8 +254,8 @@ func (a *application) rewriteSQLNode(parent SQLNode, node SQLNode, replacer repl
 		return a.rewriteRefOfNotExpr(parent, node, replacer)
 	case *NullVal:
 		return a.rewriteRefOfNullVal(parent, node, replacer)
-	case Offset:
-		return a.rewriteOffset(parent, node, replacer)
+	case *Offset:
+		return a.rewriteRefOfOffset(parent, node, replacer)
 	case OnDup:
 		return a.rewriteOnDup(parent, node, replacer)
 	case *OptLike:
@@ -3938,6 +3938,30 @@ func (a *application) rewriteRefOfNullVal(parent SQLNode, node *NullVal, replace
 	}
 	return true
 }
+func (a *application) rewriteRefOfOffset(parent SQLNode, node *Offset, replacer replacerFunc) bool {
+	if node == nil {
+		return true
+	}
+	if a.pre != nil {
+		a.cur.replacer = replacer
+		a.cur.parent = parent
+		a.cur.node = node
+		if !a.pre(&a.cur) {
+			return true
+		}
+	}
+	if a.post != nil {
+		if a.pre == nil {
+			a.cur.replacer = replacer
+			a.cur.parent = parent
+			a.cur.node = node
+		}
+		if !a.post(&a.cur) {
+			return false
+		}
+	}
+	return true
+}
 func (a *application) rewriteOnDup(parent SQLNode, node OnDup, replacer replacerFunc) bool {
 	if node == nil {
 		return true
@@ -6856,8 +6880,8 @@ func (a *application) rewriteExpr(parent SQLNode, node Expr, replacer replacerFu
 		return a.rewriteRefOfNotExpr(parent, node, replacer)
 	case *NullVal:
 		return a.rewriteRefOfNullVal(parent, node, replacer)
-	case Offset:
-		return a.rewriteOffset(parent, node, replacer)
+	case *Offset:
+		return a.rewriteRefOfOffset(parent, node, replacer)
 	case *OrExpr:
 		return a.rewriteRefOfOrExpr(parent, node, replacer)
 	case *Subquery:
@@ -6998,8 +7022,8 @@ func (a *application) rewriteJSONPathParam(parent SQLNode, node JSONPathParam, r
 		return a.rewriteRefOfNotExpr(parent, node, replacer)
 	case *NullVal:
 		return a.rewriteRefOfNullVal(parent, node, replacer)
-	case Offset:
-		return a.rewriteOffset(parent, node, replacer)
+	case *Offset:
+		return a.rewriteRefOfOffset(parent, node, replacer)
 	case *OrExpr:
 		return a.rewriteRefOfOrExpr(parent, node, replacer)
 	case *Subquery:
@@ -7350,27 +7374,6 @@ func (a *application) rewriteMatchAction(parent SQLNode, node MatchAction, repla
 	}
 	return true
 }
-func (a *application) rewriteOffset(parent SQLNode, node Offset, replacer replacerFunc) bool {
-	if a.pre != nil {
-		a.cur.replacer = replacer
-		a.cur.parent = parent
-		a.cur.node = node
-		if !a.pre(&a.cur) {
-			return true
-		}
-	}
-	if a.post != nil {
-		if a.pre == nil {
-			a.cur.replacer = replacer
-			a.cur.parent = parent
-			a.cur.node = node
-		}
-		if !a.post(&a.cur) {
-			return false
-		}
-	}
-	return true
-}
 func (a *application) rewriteReferenceAction(parent SQLNode, node ReferenceAction, replacer replacerFunc) bool {
 	if a.pre != nil {
 		a.cur.replacer = replacer
diff --git a/go/vt/sqlparser/ast_visit.go b/go/vt/sqlparser/ast_visit.go
index 8faf0f6fe7..6da9388788 100644
--- a/go/vt/sqlparser/ast_visit.go
+++ b/go/vt/sqlparser/ast_visit.go
@@ -254,8 +254,8 @@ func VisitSQLNode(in SQLNode, f Visit) error {
 		return VisitRefOfNotExpr(in, f)
 	case *NullVal:
 		return VisitRefOfNullVal(in, f)
-	case Offset:
-		return VisitOffset(in, f)
+	case *Offset:
+		return VisitRefOfOffset(in, f)
 	case OnDup:
 		return VisitOnDup(in, f)
 	case *OptLike:
@@ -2033,6 +2033,15 @@ func VisitRefOfNullVal(in *NullVal, f Visit) error {
 	}
 	return nil
 }
+func VisitRefOfOffset(in *Offset, f Visit) error {
+	if in == nil {
+		return nil
+	}
+	if cont, err := f(in); err != nil || !cont {
+		return err
+	}
+	return nil
+}
 func VisitOnDup(in OnDup, f Visit) error {
 	if in == nil {
 		return nil
@@ -3505,8 +3514,8 @@ func VisitExpr(in Expr, f Visit) error {
 		return VisitRefOfNotExpr(in, f)
 	case *NullVal:
 		return VisitRefOfNullVal(in, f)
-	case Offset:
-		return VisitOffset(in, f)
+	case *Offset:
+		return VisitRefOfOffset(in, f)
 	case *OrExpr:
 		return VisitRefOfOrExpr(in, f)
 	case *Subquery:
@@ -3647,8 +3656,8 @@ func VisitJSONPathParam(in JSONPathParam, f Visit) error {
 		return VisitRefOfNotExpr(in, f)
 	case *NullVal:
 		return VisitRefOfNullVal(in, f)
-	case Offset:
-		return VisitOffset(in, f)
+	case *Offset:
+		return VisitRefOfOffset(in, f)
 	case *OrExpr:
 		return VisitRefOfOrExpr(in, f)
 	case *Subquery:
@@ -3880,10 +3889,6 @@ func VisitMatchAction(in MatchAction, f Visit) error {
 	_, err := f(in)
 	return err
 }
-func VisitOffset(in Offset, f Visit) error {
-	_, err := f(in)
-	return err
-}
 func VisitReferenceAction(in ReferenceAction, f Visit) error {
 	_, err := f(in)
 	return err
diff --git a/go/vt/sqlparser/cached_size.go b/go/vt/sqlparser/cached_size.go
index 767efd39a7..b93fbe4f35 100644
--- a/go/vt/sqlparser/cached_size.go
+++ b/go/vt/sqlparser/cached_size.go
@@ -2104,6 +2104,18 @@ func (cached *NotExpr) CachedSize(alloc bool) int64 {
 	}
 	return size
 }
+func (cached *Offset) CachedSize(alloc bool) int64 {
+	if cached == nil {
+		return int64(0)
+	}
+	size := int64(0)
+	if alloc {
+		size += int64(24)
+	}
+	// field Original string
+	size += hack.RuntimeAllocSize(int64(len(cached.Original)))
+	return size
+}
 func (cached *OptLike) CachedSize(alloc bool) int64 {
 	if cached == nil {
 		return int64(0)
diff --git a/go/vt/vtadmin/grpcserver/server.go b/go/vt/vtadmin/grpcserver/server.go
index 65bfaaf5a3..c7e58fb072 100644
--- a/go/vt/vtadmin/grpcserver/server.go
+++ b/go/vt/vtadmin/grpcserver/server.go
@@ -22,6 +22,7 @@ import (
 	"net/http"
 	"os"
 	"os/signal"
+	"strings"
 	"sync"
 	"syscall"
 	"time"
@@ -32,6 +33,7 @@ import (
 	otgrpc "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
 	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
 	"github.com/opentracing/opentracing-go"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/soheilhy/cmux"
 	"google.golang.org/grpc"
 	channelz "google.golang.org/grpc/channelz/service"
@@ -68,6 +70,15 @@ type Options struct {
 	// gRPC server.
 	EnableChannelz bool
 
+	// MetricsEndpoint is the route to serve promhttp metrics on, including
+	// those collected be grpc_prometheus interceptors.
+	//
+	// It is the user's responsibility to ensure this does not conflict with
+	// other endpoints.
+	//
+	// Omit to not export metrics.
+	MetricsEndpoint string
+
 	StreamInterceptors []grpc.StreamServerInterceptor
 	UnaryInterceptors  []grpc.UnaryServerInterceptor
 }
@@ -195,6 +206,15 @@ func (s *Server) ListenAndServe() error { // nolint:funlen
 		shutdown <- err
 	}()
 
+	if s.opts.MetricsEndpoint != "" {
+		if !strings.HasPrefix(s.opts.MetricsEndpoint, "/") {
+			s.opts.MetricsEndpoint = "/" + s.opts.MetricsEndpoint
+		}
+
+		grpc_prometheus.Register(s.gRPCServer)
+		s.router.Handle(s.opts.MetricsEndpoint, promhttp.Handler())
+	}
+
 	// Start the servers
 	go func() {
 		err := s.gRPCServer.Serve(grpcLis)
diff --git a/go/vt/vtadmin/vtctldclient/fakevtctldclient/vtctldclient.go b/go/vt/vtadmin/vtctldclient/fakevtctldclient/vtctldclient.go
index 77c4d875a7..e98787dc43 100644
--- a/go/vt/vtadmin/vtctldclient/fakevtctldclient/vtctldclient.go
+++ b/go/vt/vtadmin/vtctldclient/fakevtctldclient/vtctldclient.go
@@ -19,6 +19,8 @@ package fakevtctldclient
 import (
 	"context"
 	"fmt"
+	"sort"
+	"strings"
 
 	"github.com/stretchr/testify/assert"
 	"google.golang.org/grpc"
@@ -35,8 +37,10 @@ import (
 type VtctldClient struct {
 	vtctldclient.VtctldClient
 
-	CreateKeyspaceShouldErr        bool
-	DeleteKeyspaceShouldErr        bool
+	CreateKeyspaceShouldErr bool
+	DeleteKeyspaceShouldErr bool
+	// Keyed by _sorted_ TabletAlias list string joined by commas.
+	DeleteTabletsResults           map[string]error
 	FindAllShardsInKeyspaceResults map[string]struct {
 		Response *vtctldatapb.FindAllShardsInKeyspaceResponse
 		Error    error
@@ -73,10 +77,7 @@ type VtctldClient struct {
 		Response *vtctldatapb.GetWorkflowsResponse
 		Error    error
 	}
-	ShardReplicationPositionsResults map[string]struct {
-		Response *vtctldatapb.ShardReplicationPositionsResponse
-		Error    error
-	}
+	RefreshStateResults         map[string]error
 	ReloadSchemaKeyspaceResults map[string]struct {
 		Response *vtctldatapb.ReloadSchemaKeyspaceResponse
 		Error    error
@@ -89,6 +90,17 @@ type VtctldClient struct {
 		Response *vtctldatapb.ReloadSchemaShardResponse
 		Error    error
 	}
+	ReparentTabletResults map[string]struct {
+		Response *vtctldatapb.ReparentTabletResponse
+		Error    error
+	}
+	SetWritableResults               map[string]error
+	ShardReplicationPositionsResults map[string]struct {
+		Response *vtctldatapb.ShardReplicationPositionsResponse
+		Error    error
+	}
+	StartReplicationResults map[string]error
+	StopReplicationResults  map[string]error
 }
 
 // Compile-time type assertion to make sure we haven't overriden a method
@@ -127,6 +139,27 @@ func (fake *VtctldClient) DeleteKeyspace(ctx context.Context, req *vtctldatapb.D
 	return &vtctldatapb.DeleteKeyspaceResponse{}, nil
 }
 
+// DeleteTablets is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) DeleteTablets(ctx context.Context, req *vtctldatapb.DeleteTabletsRequest, opts ...grpc.CallOption) (*vtctldatapb.DeleteTabletsResponse, error) {
+	if fake.DeleteTabletsResults == nil {
+		return nil, fmt.Errorf("%w: DeleteTabletsResults not set on fake vtctldclient", assert.AnError)
+	}
+
+	aliases := topoproto.TabletAliasList(req.TabletAliases)
+	sort.Sort(aliases)
+	key := strings.Join(aliases.ToStringSlice(), ",")
+
+	if err, ok := fake.DeleteTabletsResults[key]; ok {
+		if err != nil {
+			return nil, err
+		}
+
+		return &vtctldatapb.DeleteTabletsResponse{}, nil
+	}
+
+	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
+}
+
 // FindAllShardsInKeyspace is part of the vtctldclient.VtctldClient interface.
 func (fake *VtctldClient) FindAllShardsInKeyspace(ctx context.Context, req *vtctldatapb.FindAllShardsInKeyspaceRequest, opts ...grpc.CallOption) (*vtctldatapb.FindAllShardsInKeyspaceResponse, error) {
 	if fake.FindAllShardsInKeyspaceResults == nil {
@@ -240,15 +273,19 @@ func (fake *VtctldClient) GetWorkflows(ctx context.Context, req *vtctldatapb.Get
 	return nil, fmt.Errorf("%w: no result set for keyspace %s", assert.AnError, req.Keyspace)
 }
 
-// ShardReplicationPositions is part of the vtctldclient.VtctldClient interface.
-func (fake *VtctldClient) ShardReplicationPositions(ctx context.Context, req *vtctldatapb.ShardReplicationPositionsRequest, opts ...grpc.CallOption) (*vtctldatapb.ShardReplicationPositionsResponse, error) {
-	if fake.ShardReplicationPositionsResults == nil {
-		return nil, fmt.Errorf("%w: ShardReplicationPositionsResults not set on fake vtctldclient", assert.AnError)
+// RefreshState is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) RefreshState(ctx context.Context, req *vtctldatapb.RefreshStateRequest, opts ...grpc.CallOption) (*vtctldatapb.RefreshStateResponse, error) {
+	if fake.RefreshStateResults == nil {
+		return nil, fmt.Errorf("%w: RefreshStateResults not set on fake vtctldclient", assert.AnError)
 	}
 
-	key := fmt.Sprintf("%s/%s", req.Keyspace, req.Shard)
-	if result, ok := fake.ShardReplicationPositionsResults[key]; ok {
-		return result.Response, result.Error
+	key := topoproto.TabletAliasString(req.TabletAlias)
+	if err, ok := fake.RefreshStateResults[key]; ok {
+		if err != nil {
+			return nil, err
+		}
+
+		return &vtctldatapb.RefreshStateResponse{}, nil
 	}
 
 	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
@@ -294,3 +331,85 @@ func (fake *VtctldClient) ReloadSchemaShard(ctx context.Context, req *vtctldatap
 
 	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
 }
+
+// ReparentTablet is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) ReparentTablet(ctx context.Context, req *vtctldatapb.ReparentTabletRequest, opts ...grpc.CallOption) (*vtctldatapb.ReparentTabletResponse, error) {
+	if fake.ReparentTabletResults == nil {
+		return nil, fmt.Errorf("%w: ReparentTabletResults not set on fake vtctldclient", assert.AnError)
+	}
+
+	key := topoproto.TabletAliasString(req.Tablet)
+	if result, ok := fake.ReparentTabletResults[key]; ok {
+		return result.Response, result.Error
+	}
+
+	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
+}
+
+// SetWritable is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) SetWritable(ctx context.Context, req *vtctldatapb.SetWritableRequest, opts ...grpc.CallOption) (*vtctldatapb.SetWritableResponse, error) {
+	if fake.SetWritableResults == nil {
+		return nil, fmt.Errorf("%w: SetWritableResults not set on fake vtctldclient", assert.AnError)
+	}
+
+	key := topoproto.TabletAliasString(req.TabletAlias)
+	if err, ok := fake.SetWritableResults[key]; ok {
+		if err != nil {
+			return nil, err
+		}
+
+		return &vtctldatapb.SetWritableResponse{}, nil
+	}
+
+	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
+}
+
+// ShardReplicationPositions is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) ShardReplicationPositions(ctx context.Context, req *vtctldatapb.ShardReplicationPositionsRequest, opts ...grpc.CallOption) (*vtctldatapb.ShardReplicationPositionsResponse, error) {
+	if fake.ShardReplicationPositionsResults == nil {
+		return nil, fmt.Errorf("%w: ShardReplicationPositionsResults not set on fake vtctldclient", assert.AnError)
+	}
+
+	key := fmt.Sprintf("%s/%s", req.Keyspace, req.Shard)
+	if result, ok := fake.ShardReplicationPositionsResults[key]; ok {
+		return result.Response, result.Error
+	}
+
+	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
+}
+
+// StartReplication is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) StartReplication(ctx context.Context, req *vtctldatapb.StartReplicationRequest, opts ...grpc.CallOption) (*vtctldatapb.StartReplicationResponse, error) {
+	if fake.StartReplicationResults == nil {
+		return nil, fmt.Errorf("%w: StartReplicationResults not set on fake vtctldclient", assert.AnError)
+	}
+
+	key := topoproto.TabletAliasString(req.TabletAlias)
+	if err, ok := fake.StartReplicationResults[key]; ok {
+		if err != nil {
+			return nil, err
+		}
+
+		return &vtctldatapb.StartReplicationResponse{}, nil
+	}
+
+	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
+}
+
+// StopReplication is part of the vtctldclient.VtctldClient interface.
+func (fake *VtctldClient) StopReplication(ctx context.Context, req *vtctldatapb.StopReplicationRequest, opts ...grpc.CallOption) (*vtctldatapb.StopReplicationResponse, error) {
+	if fake.StopReplicationResults == nil {
+		return nil, fmt.Errorf("%w: StopReplicationResults not set on fake vtctldclient", assert.AnError)
+	}
+
+	key := topoproto.TabletAliasString(req.TabletAlias)
+	if err, ok := fake.StopReplicationResults[key]; ok {
+		if err != nil {
+			return nil, err
+		}
+
+		return &vtctldatapb.StopReplicationResponse{}, nil
+	}
+
+	return nil, fmt.Errorf("%w: no result set for %s", assert.AnError, key)
+}
diff --git a/go/vt/vtgate/evalengine/translate.go b/go/vt/vtgate/evalengine/translate.go
index 6a5d9ba507..4239d0df06 100644
--- a/go/vt/vtgate/evalengine/translate.go
+++ b/go/vt/vtgate/evalengine/translate.go
@@ -572,8 +572,8 @@ func translateExpr(e sqlparser.Expr, lookup TranslationLookup) (Expr, error) {
 		return NewLiteralInt(0), nil
 	case *sqlparser.ColName:
 		return translateColName(node, lookup)
-	case sqlparser.Offset:
-		return NewColumn(int(node), getCollation(node, lookup)), nil
+	case *sqlparser.Offset:
+		return NewColumn(node.V, getCollation(node, lookup)), nil
 	case *sqlparser.ComparisonExpr:
 		return translateComparisonExpr(node.Operator, node.Left, node.Right, lookup)
 	case sqlparser.Argument:
diff --git a/go/vt/vtgate/planbuilder/abstract/queryprojection.go b/go/vt/vtgate/planbuilder/abstract/queryprojection.go
index f91c3ae576..7ba9fde31a 100644
--- a/go/vt/vtgate/planbuilder/abstract/queryprojection.go
+++ b/go/vt/vtgate/planbuilder/abstract/queryprojection.go
@@ -45,6 +45,9 @@ type (
 		OrderExprs         []OrderBy
 		CanPushDownSorting bool
 		HasStar            bool
+
+		// AddedColumn keeps a counter for expressions added to solve HAVING expressions the user is not selecting
+		AddedColumn int
 	}
 
 	// OrderBy contains the expression to used in order by and also if ordering is needed at VTGate level then what the weight_string function expression to be sent down for evaluation.
@@ -65,15 +68,20 @@ type (
 		aliasedExpr *sqlparser.AliasedExpr
 	}
 
+	// Aggr encodes all information needed for aggregation functions
 	Aggr struct {
 		Original *sqlparser.AliasedExpr
-		Func     *sqlparser.FuncExpr
 		OpCode   engine.AggregateOpcode
 		Alias    string
 		// The index at which the user expects to see this aggregated function. Set to nil, if the user does not ask for it
 		Index    *int
 		Distinct bool
 	}
+
+	AggrRewriter struct {
+		qp  *QueryProjection
+		Err error
+	}
 )
 
 func (b GroupBy) AsOrderBy() OrderBy {
@@ -172,6 +180,47 @@ func CreateQPFromSelect(sel *sqlparser.Select) (*QueryProjection, error) {
 	return qp, nil
 }
 
+// Rewrite will go through an expression, add aggregations to the QP, and rewrite them to use column offset
+func (ar *AggrRewriter) Rewrite() func(*sqlparser.Cursor) bool {
+	return func(cursor *sqlparser.Cursor) bool {
+		if ar.Err != nil {
+			return false
+		}
+		sqlNode := cursor.Node()
+		if sqlparser.IsAggregation(sqlNode) {
+			fExp := sqlNode.(*sqlparser.FuncExpr)
+			for offset, expr := range ar.qp.SelectExprs {
+				ae, err := expr.GetAliasedExpr()
+				if err != nil {
+					ar.Err = err
+					return false
+				}
+				if sqlparser.EqualsExpr(ae.Expr, fExp) {
+					cursor.Replace(sqlparser.NewOffset(offset, fExp))
+					return false // no need to visit aggregation children
+				}
+			}
+
+			col := SelectExpr{
+				Aggr: true,
+				Col:  &sqlparser.AliasedExpr{Expr: fExp},
+			}
+			ar.qp.HasAggr = true
+
+			cursor.Replace(sqlparser.NewOffset(len(ar.qp.SelectExprs), fExp))
+			ar.qp.SelectExprs = append(ar.qp.SelectExprs, col)
+			ar.qp.AddedColumn++
+		}
+
+		return true
+	}
+}
+
+// AggrRewriter extracts
+func (qp *QueryProjection) AggrRewriter() *AggrRewriter {
+	return &AggrRewriter{qp: qp}
+}
+
 func (qp *QueryProjection) addSelectExpressions(sel *sqlparser.Select) error {
 	for _, selExp := range sel.SelectExprs {
 		switch selExp := selExp.(type) {
@@ -445,7 +494,6 @@ func (qp *QueryProjection) AggregationExpressions() (out []Aggr, err error) {
 
 		out = append(out, Aggr{
 			Original: aliasedExpr,
-			Func:     fExpr,
 			OpCode:   opcode,
 			Alias:    aliasedExpr.ColumnName(),
 			Index:    &idxCopy,
@@ -513,7 +561,7 @@ func (qp *QueryProjection) AlignGroupByAndOrderBy() {
 		// The query didn't ask for any particular order, so we are free to add arbitrary ordering.
 		// We'll align the grouping and ordering by the output columns
 		newGrouping = qp.GetGrouping()
-		sort.Sort(GroupBys(newGrouping))
+		SortGrouping(newGrouping)
 		for _, groupBy := range newGrouping {
 			qp.OrderExprs = append(qp.OrderExprs, groupBy.AsOrderBy())
 		}
@@ -549,6 +597,10 @@ func (qp *QueryProjection) AddGroupBy(by GroupBy) {
 	qp.groupByExprs = append(qp.groupByExprs, by)
 }
 
+func (qp *QueryProjection) GetColumnCount() int {
+	return len(qp.SelectExprs) - qp.AddedColumn
+}
+
 func checkForInvalidGroupingExpressions(expr sqlparser.Expr) error {
 	return sqlparser.Walk(func(node sqlparser.SQLNode) (bool, error) {
 		if sqlparser.IsAggregation(node) {
@@ -563,21 +615,16 @@ func checkForInvalidGroupingExpressions(expr sqlparser.Expr) error {
 	}, expr)
 }
 
-type Aggrs []Aggr
-
-// Len implements the sort.Interface
-func (a Aggrs) Len() int {
-	return len(a)
-}
-
-// Less implements the sort.Interface
-func (a Aggrs) Less(i, j int) bool {
-	return CompareRefInt(a[i].Index, a[j].Index)
+func SortAggregations(a []Aggr) {
+	sort.Slice(a, func(i, j int) bool {
+		return CompareRefInt(a[i].Index, a[j].Index)
+	})
 }
 
-// Swap implements the sort.Interface
-func (a Aggrs) Swap(i, j int) {
-	a[i], a[j] = a[j], a[i]
+func SortGrouping(a []GroupBy) {
+	sort.Slice(a, func(i, j int) bool {
+		return CompareRefInt(a[i].InnerIndex, a[j].InnerIndex)
+	})
 }
 
 // CompareRefInt compares two references of integers.
@@ -591,20 +638,3 @@ func CompareRefInt(a *int, b *int) bool {
 	}
 	return *a < *b
 }
-
-type GroupBys []GroupBy
-
-// Len implements the sort.Interface
-func (gbys GroupBys) Len() int {
-	return len(gbys)
-}
-
-// Less implements the sort.Interface
-func (gbys GroupBys) Less(i, j int) bool {
-	return CompareRefInt(gbys[i].InnerIndex, gbys[j].InnerIndex)
-}
-
-// Swap implements the sort.Interface
-func (gbys GroupBys) Swap(i, j int) {
-	gbys[i], gbys[j] = gbys[j], gbys[i]
-}
diff --git a/go/vt/vtgate/planbuilder/aggregation_pushing.go b/go/vt/vtgate/planbuilder/aggregation_pushing.go
index bae6486f2b..cb937910d8 100644
--- a/go/vt/vtgate/planbuilder/aggregation_pushing.go
+++ b/go/vt/vtgate/planbuilder/aggregation_pushing.go
@@ -17,8 +17,6 @@ limitations under the License.
 package planbuilder
 
 import (
-	"sort"
-
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
@@ -168,7 +166,7 @@ func addAggregationToSelect(sel *sqlparser.Select, aggregation abstract.Aggr) of
 		if !isAliasedExpr {
 			continue
 		}
-		if sqlparser.EqualsExpr(aliasedExpr.Expr, aggregation.Func) {
+		if sqlparser.EqualsExpr(aliasedExpr.Expr, aggregation.Original.Expr) {
 			return newOffset(i)
 		}
 	}
@@ -186,7 +184,6 @@ func countStarAggr() *abstract.Aggr {
 
 	return &abstract.Aggr{
 		Original: &sqlparser.AliasedExpr{Expr: f},
-		Func:     f,
 		OpCode:   engine.AggregateCount,
 		Alias:    "count(*)",
 	}
@@ -377,11 +374,11 @@ func splitAggregationsToLeftAndRight(
 	var lhsAggrs, rhsAggrs []*abstract.Aggr
 	for _, aggr := range aggregations {
 		newAggr := aggr
-		if isCountStar(aggr.Func) {
+		if isCountStar(aggr.Original.Expr) {
 			lhsAggrs = append(lhsAggrs, &newAggr)
 			rhsAggrs = append(rhsAggrs, &newAggr)
 		} else {
-			deps := ctx.SemTable.RecursiveDeps(aggr.Func)
+			deps := ctx.SemTable.RecursiveDeps(aggr.Original.Expr)
 			var other *abstract.Aggr
 			// if we are sending down min/max, we don't have to multiply the results with anything
 			if !isMinOrMax(aggr.OpCode) {
@@ -484,8 +481,8 @@ func sortOffsets(grouping []abstract.GroupBy, aggregations []abstract.Aggr) ([]a
 	originalAggr := make([]abstract.Aggr, len(aggregations))
 	copy(originalAggr, aggregations)
 	copy(originalGrouping, grouping)
-	sort.Sort(abstract.Aggrs(aggregations))
-	sort.Sort(abstract.GroupBys(grouping))
+	abstract.SortAggregations(aggregations)
+	abstract.SortGrouping(grouping)
 
 	reorg := func(groupByOffsets []offsets, aggrOffsets [][]offsets) ([]offsets, [][]offsets) {
 		orderedGroupingOffsets := make([]offsets, 0, len(originalGrouping))
@@ -501,7 +498,7 @@ func sortOffsets(grouping []abstract.GroupBy, aggregations []abstract.Aggr) ([]a
 		orderedAggrs := make([][]offsets, 0, len(originalAggr))
 		for _, og := range originalAggr {
 			for i, g := range aggregations {
-				if og.Func == g.Func {
+				if og.Original.Expr == g.Original.Expr {
 					orderedAggrs = append(orderedAggrs, aggrOffsets[i])
 					break
 				}
diff --git a/go/vt/vtgate/planbuilder/horizon_planning.go b/go/vt/vtgate/planbuilder/horizon_planning.go
index fe9f8f58c9..81945b1f41 100644
--- a/go/vt/vtgate/planbuilder/horizon_planning.go
+++ b/go/vt/vtgate/planbuilder/horizon_planning.go
@@ -17,6 +17,7 @@ limitations under the License.
 package planbuilder
 
 import (
+	"vitess.io/vitess/go/mysql/collations"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/abstract"
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/physical"
@@ -138,18 +139,18 @@ func pushProjections(ctx *plancontext.PlanningContext, plan logicalPlan, selectE
 }
 
 func (hp *horizonPlanning) truncateColumnsIfNeeded(ctx *plancontext.PlanningContext, plan logicalPlan) (logicalPlan, error) {
-	if len(plan.OutputColumns()) == hp.sel.GetColumnCount() {
+	if len(plan.OutputColumns()) == hp.qp.GetColumnCount() {
 		return plan, nil
 	}
 	switch p := plan.(type) {
 	case *routeGen4:
-		p.eroute.SetTruncateColumnCount(hp.sel.GetColumnCount())
+		p.eroute.SetTruncateColumnCount(hp.qp.GetColumnCount())
 	case *joinGen4, *semiJoin, *hashJoin:
 		// since this is a join, we can safely add extra columns and not need to truncate them
 	case *orderedAggregate:
-		p.truncateColumnCount = hp.sel.GetColumnCount()
+		p.truncateColumnCount = hp.qp.GetColumnCount()
 	case *memorySort:
-		p.truncater.SetTruncateColumnCount(hp.sel.GetColumnCount())
+		p.truncater.SetTruncateColumnCount(hp.qp.GetColumnCount())
 	case *pulloutSubquery:
 		newUnderlyingPlan, err := hp.truncateColumnsIfNeeded(ctx, p.underlying)
 		if err != nil {
@@ -162,7 +163,8 @@ func (hp *horizonPlanning) truncateColumnsIfNeeded(ctx *plancontext.PlanningCont
 			eSimpleProj:       &engine.SimpleProjection{},
 		}
 
-		err := pushProjections(ctx, plan, hp.qp.SelectExprs)
+		exprs := hp.qp.SelectExprs[0:hp.qp.GetColumnCount()]
+		err := pushProjections(ctx, plan, exprs)
 		if err != nil {
 			return nil, err
 		}
@@ -321,7 +323,18 @@ func pushProjection(
 				return key.KeyCol, false, nil
 			}
 		}
-		return 0, false, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "cannot push projections in ordered aggregates")
+		offset, _, err := pushProjection(ctx, expr, node.input, inner, true, hasAggregation)
+		if err != nil {
+			return 0, false, err
+		}
+		node.aggregates = append(node.aggregates, &engine.AggregateParams{
+			Opcode:   engine.AggregateRandom,
+			Col:      offset,
+			Alias:    expr.ColumnName(),
+			Expr:     expr.Expr,
+			Original: expr,
+		})
+		return offset, true, nil
 	case *vindexFunc:
 		colsBefore := len(node.eVindexFunc.Cols)
 		i, err := node.SupplyProjection(expr, reuseCol)
@@ -528,6 +541,14 @@ func (hp *horizonPlanning) planAggrUsingOA(
 		})
 	}
 
+	if hp.sel.Having != nil {
+		rewriter := hp.qp.AggrRewriter()
+		sqlparser.Rewrite(hp.sel.Having.Expr, rewriter.Rewrite(), nil)
+		if rewriter.Err != nil {
+			return nil, rewriter.Err
+		}
+	}
+
 	aggregationExprs, err := hp.qp.AggregationExpressions()
 	if err != nil {
 		return nil, err
@@ -600,13 +621,13 @@ func passGroupingColumns(proj *projection, groupings []offsets, grouping []abstr
 	for idx, grp := range groupings {
 		origGrp := grouping[idx]
 		var offs offsets
-		alias := origGrp.AsAliasedExpr().ColumnName()
-		offs.col, err = proj.addColumn(origGrp.InnerIndex, sqlparser.Offset(grp.col), alias)
+		expr := origGrp.AsAliasedExpr()
+		offs.col, err = proj.addColumn(origGrp.InnerIndex, sqlparser.NewOffset(grp.col, expr.Expr), expr.ColumnName())
 		if err != nil {
 			return nil, err
 		}
 		if grp.wsCol != -1 {
-			offs.wsCol, err = proj.addColumn(nil, sqlparser.Offset(grp.wsCol), "")
+			offs.wsCol, err = proj.addColumn(nil, sqlparser.NewOffset(grp.wsCol, weightStringFor(expr.Expr)), "")
 			if err != nil {
 				return nil, err
 			}
@@ -625,7 +646,7 @@ func generateAggregateParams(aggrs []abstract.Aggr, aggrParamOffsets [][]offsets
 		if proj != nil {
 			var aggrExpr sqlparser.Expr
 			for _, ofs := range paramOffset {
-				curr := sqlparser.Offset(ofs.col)
+				curr := &sqlparser.Offset{V: ofs.col}
 				if aggrExpr == nil {
 					aggrExpr = curr
 				} else {
@@ -657,7 +678,7 @@ func generateAggregateParams(aggrs []abstract.Aggr, aggrParamOffsets [][]offsets
 			Opcode:   opcode,
 			Col:      offset,
 			Alias:    aggr.Alias,
-			Expr:     aggr.Func,
+			Expr:     aggr.Original.Expr,
 			Original: aggr.Original,
 		}
 	}
@@ -689,7 +710,11 @@ func addColumnsToOA(
 			o := groupings[count]
 			count++
 			a := aggregationExprs[offset]
-			collID := ctx.SemTable.CollationForExpr(a.Func.Exprs[0].(*sqlparser.AliasedExpr).Expr)
+			collID := collations.Unknown
+			fnc, ok := a.Original.Expr.(*sqlparser.FuncExpr)
+			if ok {
+				collID = ctx.SemTable.CollationForExpr(fnc.Exprs[0].(*sqlparser.AliasedExpr).Expr)
+			}
 			oa.aggregates = append(oa.aggregates, &engine.AggregateParams{
 				Opcode:      a.OpCode,
 				Col:         o.col,
@@ -697,7 +722,6 @@ func addColumnsToOA(
 				WAssigned:   o.wsCol >= 0,
 				WCol:        o.wsCol,
 				Alias:       a.Alias,
-				Expr:        a.Func,
 				Original:    a.Original,
 				CollationID: collID,
 			})
@@ -736,7 +760,8 @@ func (hp *horizonPlanning) handleDistinctAggr(ctx *plancontext.PlanningContext,
 			aggrs = append(aggrs, expr)
 			continue
 		}
-		aliasedExpr, ok := expr.Func.Exprs[0].(*sqlparser.AliasedExpr)
+		funcExpr := expr.Original.Expr.(*sqlparser.FuncExpr) // we wouldn't be in this method if this wasn't a function
+		aliasedExpr, ok := funcExpr.Exprs[0].(*sqlparser.AliasedExpr)
 		if !ok {
 			err = vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "syntax error: %s", sqlparser.String(expr.Original))
 			return
@@ -813,8 +838,9 @@ func (hp *horizonPlanning) createGroupingsForColumns(columns []*sqlparser.ColNam
 	return lhsGrouping, nil
 }
 
-func isCountStar(f *sqlparser.FuncExpr) bool {
-	if f == nil {
+func isCountStar(e sqlparser.Expr) bool {
+	f, ok := e.(*sqlparser.FuncExpr)
+	if !ok || !f.Name.EqualString("count") {
 		return false
 	}
 	_, isStar := f.Exprs[0].(*sqlparser.StarExpr)
@@ -1340,9 +1366,6 @@ func pushHaving(ctx *plancontext.PlanningContext, expr sqlparser.Expr, plan logi
 	case *simpleProjection:
 		return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: filtering on results of cross-shard derived table")
 	case *orderedAggregate:
-		if sqlparser.ContainsAggregation(expr) {
-			expr = sqlparser.Rewrite(expr, node.rewriteAggrExpressions(), nil).(sqlparser.Expr)
-		}
 		return newFilter(ctx, plan, expr)
 	}
 	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] unreachable %T.filtering", plan)
diff --git a/go/vt/vtgate/planbuilder/ordered_aggregate.go b/go/vt/vtgate/planbuilder/ordered_aggregate.go
index 8a940e340d..c79120a8a8 100644
--- a/go/vt/vtgate/planbuilder/ordered_aggregate.go
+++ b/go/vt/vtgate/planbuilder/ordered_aggregate.go
@@ -387,20 +387,3 @@ func (oa *orderedAggregate) OutputColumns() []sqlparser.SelectExpr {
 func (oa *orderedAggregate) SetTruncateColumnCount(count int) {
 	oa.truncateColumnCount = count
 }
-
-// rewriteAggrExpressions is used when our predicate expression contains aggregation.
-// In these cases, we need to rewrite it, so it uses the column output from the ordered aggregate
-func (oa *orderedAggregate) rewriteAggrExpressions() func(*sqlparser.Cursor) bool {
-	return func(cursor *sqlparser.Cursor) bool {
-		sqlNode := cursor.Node()
-		if sqlparser.IsAggregation(sqlNode) {
-			fExp := sqlNode.(*sqlparser.FuncExpr)
-			for _, aggregate := range oa.aggregates {
-				if sqlparser.EqualsExpr(aggregate.Expr, fExp) {
-					cursor.Replace(sqlparser.Offset(aggregate.Col))
-				}
-			}
-		}
-		return true
-	}
-}
diff --git a/web/vtadmin/README.md b/web/vtadmin/README.md
index 06147382af..c5fd744ae2 100644
--- a/web/vtadmin/README.md
+++ b/web/vtadmin/README.md
@@ -17,7 +17,6 @@ Scripts for common and not-so-common tasks. These are always run from the `vites
 | `npm run lint` | Run all of the linters and formatters. The `package.json` file defines a bunch more scripts to run individual linters, if you prefer, like `npm run lint:eslint`. |
 | `npm run lint:fix` | Run all of the linters and fix errors (where possible) in place. Note that this will overwrite your files so you may want to consider committing your work beforehand! |
 | `npm run build` | Generates a build of vtadmin-web for production and outputs the files to the `vitess/web/vtadmin/build` folder. In most cases, you won't need to run this locally, but it _can_ be useful for debugging production-specific issues. See the create-react-app documentation about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. |
-| `npm run analyze` | Analyze and debug JavaScript build size using [source-map-explorer](https://create-react-app.dev/docs/analyzing-the-bundle-size/). In most cases, you'll first want to run `npm run build` to update the `build/` directory. |
 
 ## Toolchain
 
diff --git a/web/vtadmin/package-lock.json b/web/vtadmin/package-lock.json
index fa7d0d2bb3..e8ca296658 100644
--- a/web/vtadmin/package-lock.json
+++ b/web/vtadmin/package-lock.json
@@ -34,7 +34,6 @@
         "react-tiny-popover": "^6.0.5",
         "react-toastify": "^8.1.0",
         "sass": "^1.43.4",
-        "source-map-explorer": "^2.5.2",
         "svgo": "^2.8.0",
         "typescript": "^4.5.4",
         "web-vitals": "^0.2.4"
@@ -6052,17 +6051,6 @@
         "node-int64": "^0.4.0"
       }
     },
-    "node_modules/btoa": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
-      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
-      "bin": {
-        "btoa": "bin/btoa.js"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
     "node_modules/buffer": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -15562,21 +15550,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/open": {
-      "version": "7.4.2",
-      "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
-      "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
-      "dependencies": {
-        "is-docker": "^2.0.0",
-        "is-wsl": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/optionator": {
       "version": "0.9.1",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -18477,104 +18450,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/source-map-explorer": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/source-map-explorer/-/source-map-explorer-2.5.2.tgz",
-      "integrity": "sha512-gBwOyCcHPHcdLbgw6Y6kgoH1uLKL6hN3zz0xJcNI2lpnElZliIlmSYAjUVwAWnc7+HscoTyh1ScR7ITtFuEnxg==",
-      "dependencies": {
-        "btoa": "^1.2.1",
-        "chalk": "^4.1.0",
-        "convert-source-map": "^1.7.0",
-        "ejs": "^3.1.5",
-        "escape-html": "^1.0.3",
-        "glob": "^7.1.6",
-        "gzip-size": "^6.0.0",
-        "lodash": "^4.17.20",
-        "open": "^7.3.1",
-        "source-map": "^0.7.3",
-        "temp": "^0.9.4",
-        "yargs": "^16.2.0"
-      },
-      "bin": {
-        "sme": "bin/cli.js",
-        "source-map-explorer": "bin/cli.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/source-map-explorer/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/source-map-explorer/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/source-map-explorer/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/source-map-explorer/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
-    },
-    "node_modules/source-map-explorer/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/source-map-explorer/node_modules/source-map": {
-      "version": "0.7.3",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
-      "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/source-map-explorer/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/source-map-js": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -19462,18 +19337,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/temp": {
-      "version": "0.9.4",
-      "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
-      "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
-      "dependencies": {
-        "mkdirp": "^0.5.1",
-        "rimraf": "~2.6.2"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/temp-dir": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@@ -19482,17 +19345,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/temp/node_modules/rimraf": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
-      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
-      "dependencies": {
-        "glob": "^7.1.3"
-      },
-      "bin": {
-        "rimraf": "bin.js"
-      }
-    },
     "node_modules/tempy": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz",
@@ -25578,11 +25430,6 @@
         "node-int64": "^0.4.0"
       }
     },
-    "btoa": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
-      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g=="
-    },
     "buffer": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -32498,15 +32345,6 @@
         "mimic-fn": "^2.1.0"
       }
     },
-    "open": {
-      "version": "7.4.2",
-      "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
-      "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
-      "requires": {
-        "is-docker": "^2.0.0",
-        "is-wsl": "^2.1.1"
-      }
-    },
     "optionator": {
       "version": "0.9.1",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -34608,75 +34446,6 @@
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
       "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
     },
-    "source-map-explorer": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/source-map-explorer/-/source-map-explorer-2.5.2.tgz",
-      "integrity": "sha512-gBwOyCcHPHcdLbgw6Y6kgoH1uLKL6hN3zz0xJcNI2lpnElZliIlmSYAjUVwAWnc7+HscoTyh1ScR7ITtFuEnxg==",
-      "requires": {
-        "btoa": "^1.2.1",
-        "chalk": "^4.1.0",
-        "convert-source-map": "^1.7.0",
-        "ejs": "^3.1.5",
-        "escape-html": "^1.0.3",
-        "glob": "^7.1.6",
-        "gzip-size": "^6.0.0",
-        "lodash": "^4.17.20",
-        "open": "^7.3.1",
-        "source-map": "^0.7.3",
-        "temp": "^0.9.4",
-        "yargs": "^16.2.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
-        },
-        "source-map": {
-          "version": "0.7.3",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
-          "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
     "source-map-js": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -35343,25 +35112,6 @@
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
       "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
     },
-    "temp": {
-      "version": "0.9.4",
-      "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
-      "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
-      "requires": {
-        "mkdirp": "^0.5.1",
-        "rimraf": "~2.6.2"
-      },
-      "dependencies": {
-        "rimraf": {
-          "version": "2.6.3",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
-          "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
-          "requires": {
-            "glob": "^7.1.3"
-          }
-        }
-      }
-    },
     "temp-dir": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
diff --git a/web/vtadmin/package.json b/web/vtadmin/package.json
index 4ff0e8023f..a2c2601934 100644
--- a/web/vtadmin/package.json
+++ b/web/vtadmin/package.json
@@ -33,7 +33,6 @@
     "react-tiny-popover": "^6.0.5",
     "react-toastify": "^8.1.0",
     "sass": "^1.43.4",
-    "source-map-explorer": "^2.5.2",
     "svgo": "^2.8.0",
     "typescript": "^4.5.4",
     "web-vitals": "^0.2.4"
@@ -51,7 +50,6 @@
     "lint:prettier:fix": "npm run lint:prettier -- --write",
     "lint:stylelint": "stylelint \"**/*.{css,scss}\"",
     "lint:stylelint:fix": "npm run lint:stylelint -- --fix",
-    "analyze": "source-map-explorer 'build/static/js/*.js'",
     "eject": "react-scripts eject"
   },
   "eslintConfig": {
diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts
index 6d68d4fadf..9d3650dbe8 100644
--- a/web/vtadmin/src/api/http.ts
+++ b/web/vtadmin/src/api/http.ts
@@ -518,3 +518,46 @@ export const fetchShardReplicationPositions = async ({
 
     return pb.GetShardReplicationPositionsResponse.create(result);
 };
+
+export interface ReloadSchemaParams {
+    clusterIDs?: (string | null | undefined)[];
+    concurrency?: number;
+    includePrimary?: boolean;
+    keyspaces?: (string | null | undefined)[];
+
+    // e.g., ["commerce/0"]
+    keyspaceShards?: (string | null | undefined)[];
+
+    // A list of tablet aliases; e.g., ["zone1-101", "zone1-102"]
+    tablets?: (string | null | undefined)[];
+
+    waitPosition?: string;
+}
+
+export const reloadSchema = async (params: ReloadSchemaParams) => {
+    const req = new URLSearchParams();
+
+    (params.clusterIDs || []).forEach((c) => c && req.append('cluster', c));
+    (params.keyspaces || []).forEach((k) => k && req.append('keyspace', k));
+    (params.keyspaceShards || []).forEach((k) => k && req.append('keyspaceShard', k));
+    (params.tablets || []).forEach((t) => t && req.append('tablet', t));
+
+    if (typeof params.concurrency === 'number') {
+        req.append('concurrency', params.concurrency.toString());
+    }
+
+    if (typeof params.includePrimary === 'boolean') {
+        req.append('include_primary', params.includePrimary.toString());
+    }
+
+    if (typeof params.waitPosition === 'string') {
+        req.append('wait_position', params.waitPosition);
+    }
+
+    const { result } = await vtfetch(`/api/schemas/reload?${req}`, { method: 'put' });
+
+    const err = pb.ReloadSchemasResponse.verify(result);
+    if (err) throw Error(err);
+
+    return pb.ReloadSchemasResponse.create(result);
+};
diff --git a/web/vtadmin/src/components/App.tsx b/web/vtadmin/src/components/App.tsx
index 15d18b4921..fb65606043 100644
--- a/web/vtadmin/src/components/App.tsx
+++ b/web/vtadmin/src/components/App.tsx
@@ -119,7 +119,7 @@ export const App = () => {
                             <Settings />
                         </Route>
 
-                        <Redirect exact from="/" to="/tablets" />
+                        <Redirect exact from="/" to="/schemas" />
 
                         <Route>
                             <Error404 />
diff --git a/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx b/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx
index acdc68bf0d..abb4bbe72e 100644
--- a/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx
+++ b/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx
@@ -18,13 +18,17 @@ import { UseQueryResult } from 'react-query';
 import { Spinner } from '../Spinner';
 
 interface Props {
-    query: UseQueryResult;
+    query?: UseQueryResult;
+    queries?: UseQueryResult[];
 }
 
 /**
  * QueryLoadingPlaceholder is a straightforward component that displays a loading
- * message when given a query from a useQueryHook. To simplify its use, this component
- * takes care of hiding itself when `props.query` is in any state other than "loading".
+ * message when given one or more queries as returned from useQueryHook.
+ *
+ * To simplify its use, this component takes care of hiding itself when all of its
+ * queries have completed loading.
+ *
  * It's perfectly fine to render it this:
  *
  *      <QueryLoadingPlaceholder query={query} ... />
@@ -34,16 +38,22 @@ interface Props {
  *      {query.isLoading && <QueryLoadingPlaceholder query={query} ... />}
  */
 export const QueryLoadingPlaceholder: React.FC<Props> = (props) => {
-    if (!props.query.isLoading) {
+    const queries = props.queries || [];
+    if (props.query) {
+        queries.push(props.query);
+    }
+
+    const anyLoading = queries.some((q) => q.isLoading);
+    if (!anyLoading) {
         return null;
     }
 
+    const maxFailureCount = Math.max(...queries.map((q) => q.failureCount));
+
     return (
         <div aria-busy="true" className="text-center my-12" role="status">
             <Spinner />
-            <div className="my-4 text-secondary">
-                {props.query.failureCount > 2 ? 'Still loading...' : 'Loading...'}
-            </div>
+            <div className="my-4 text-secondary">{maxFailureCount > 2 ? 'Still loading...' : 'Loading...'}</div>
         </div>
     );
 };
diff --git a/web/vtadmin/src/components/routes/Clusters.tsx b/web/vtadmin/src/components/routes/Clusters.tsx
index b40892f4d0..5d2cf439a9 100644
--- a/web/vtadmin/src/components/routes/Clusters.tsx
+++ b/web/vtadmin/src/components/routes/Clusters.tsx
@@ -24,14 +24,15 @@ import { DataCell } from '../dataTable/DataCell';
 import { ContentContainer } from '../layout/ContentContainer';
 import { WorkspaceHeader } from '../layout/WorkspaceHeader';
 import { WorkspaceTitle } from '../layout/WorkspaceTitle';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
 
 export const Clusters = () => {
     useDocumentTitle('Clusters');
-    const { data } = useClusters();
+    const clustersQuery = useClusters();
 
     const rows = React.useMemo(() => {
-        return orderBy(data, ['name']);
-    }, [data]);
+        return orderBy(clustersQuery.data, ['name']);
+    }, [clustersQuery.data]);
 
     const renderRows = (rows: pb.Cluster[]) =>
         rows.map((cluster, idx) => (
@@ -50,6 +51,7 @@ export const Clusters = () => {
             <ContentContainer>
                 <div className="max-w-screen-sm">
                     <DataTable columns={['Name', 'Id']} data={rows} renderRows={renderRows} />
+                    <QueryLoadingPlaceholder query={clustersQuery} />
                 </div>
             </ContentContainer>
         </div>
diff --git a/web/vtadmin/src/components/routes/Gates.tsx b/web/vtadmin/src/components/routes/Gates.tsx
index 42907875dc..354d4507a3 100644
--- a/web/vtadmin/src/components/routes/Gates.tsx
+++ b/web/vtadmin/src/components/routes/Gates.tsx
@@ -26,15 +26,16 @@ import { DataTable } from '../dataTable/DataTable';
 import { ContentContainer } from '../layout/ContentContainer';
 import { WorkspaceHeader } from '../layout/WorkspaceHeader';
 import { WorkspaceTitle } from '../layout/WorkspaceTitle';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
 
 export const Gates = () => {
     useDocumentTitle('Gates');
 
-    const { data } = useGates();
+    const gatesQuery = useGates();
     const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
 
     const rows = React.useMemo(() => {
-        const mapped = (data || []).map((g) => ({
+        const mapped = (gatesQuery.data || []).map((g) => ({
             cell: g.cell,
             cluster: g.cluster?.name,
             hostname: g.hostname,
@@ -43,7 +44,7 @@ export const Gates = () => {
         }));
         const filtered = filterNouns(filter, mapped);
         return orderBy(filtered, ['cluster', 'pool', 'hostname', 'cell']);
-    }, [data, filter]);
+    }, [gatesQuery.data, filter]);
 
     const renderRows = (gates: typeof rows) =>
         gates.map((gate, idx) => (
@@ -72,6 +73,7 @@ export const Gates = () => {
                     value={filter || ''}
                 />
                 <DataTable columns={['Pool', 'Hostname', 'Cell', 'Keyspaces']} data={rows} renderRows={renderRows} />
+                <QueryLoadingPlaceholder query={gatesQuery} />
             </ContentContainer>
         </div>
     );
diff --git a/web/vtadmin/src/components/routes/Keyspaces.tsx b/web/vtadmin/src/components/routes/Keyspaces.tsx
index 6d9ef9158b..88e45885f6 100644
--- a/web/vtadmin/src/components/routes/Keyspaces.tsx
+++ b/web/vtadmin/src/components/routes/Keyspaces.tsx
@@ -33,15 +33,16 @@ import KeyspaceActions from './keyspaces/KeyspaceActions';
 import { ReadOnlyGate } from '../ReadOnlyGate';
 import { isReadOnlyMode } from '../../util/env';
 import { Link } from 'react-router-dom';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
 
 export const Keyspaces = () => {
     useDocumentTitle('Keyspaces');
     const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
 
-    const { data } = useKeyspaces();
+    const keyspacesQuery = useKeyspaces();
 
     const ksRows = React.useMemo(() => {
-        const mapped = (data || []).map((k) => {
+        const mapped = (keyspacesQuery.data || []).map((k) => {
             const shardsByState = getShardsByState(k);
 
             return {
@@ -54,7 +55,7 @@ export const Keyspaces = () => {
         });
         const filtered = filterNouns(filter, mapped);
         return orderBy(filtered, ['cluster', 'name']);
-    }, [data, filter]);
+    }, [keyspacesQuery.data, filter]);
 
     const renderRows = (rows: typeof ksRows) =>
         rows.map((row, idx) => (
@@ -114,6 +115,7 @@ export const Keyspaces = () => {
                         data={ksRows}
                         renderRows={renderRows}
                     />
+                    <QueryLoadingPlaceholder query={keyspacesQuery} />
                 </div>
             </ContentContainer>
         </div>
diff --git a/web/vtadmin/src/components/routes/Schemas.tsx b/web/vtadmin/src/components/routes/Schemas.tsx
index 6d397e9a3a..fff3bb8b2d 100644
--- a/web/vtadmin/src/components/routes/Schemas.tsx
+++ b/web/vtadmin/src/components/routes/Schemas.tsx
@@ -30,6 +30,7 @@ import { ContentContainer } from '../layout/ContentContainer';
 import { WorkspaceHeader } from '../layout/WorkspaceHeader';
 import { WorkspaceTitle } from '../layout/WorkspaceTitle';
 import { KeyspaceLink } from '../links/KeyspaceLink';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
 import { HelpTooltip } from '../tooltip/HelpTooltip';
 
 const TABLE_COLUMNS = [
@@ -62,11 +63,11 @@ const TABLE_COLUMNS = [
 export const Schemas = () => {
     useDocumentTitle('Schemas');
 
-    const { data = [] } = useSchemas();
+    const schemasQuery = useSchemas();
     const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
 
     const filteredData = React.useMemo(() => {
-        const tableDefinitions = getTableDefinitions(data);
+        const tableDefinitions = getTableDefinitions(schemasQuery.data);
 
         const mapped = tableDefinitions.map((d) => ({
             cluster: d.cluster?.name,
@@ -78,7 +79,7 @@ export const Schemas = () => {
 
         const filtered = filterNouns(filter, mapped);
         return orderBy(filtered, ['cluster', 'keyspace', 'table']);
-    }, [data, filter]);
+    }, [schemasQuery.data, filter]);
 
     const renderRows = (rows: typeof filteredData) =>
         rows.map((row, idx) => {
@@ -120,6 +121,7 @@ export const Schemas = () => {
                     value={filter || ''}
                 />
                 <DataTable columns={TABLE_COLUMNS} data={filteredData} renderRows={renderRows} />
+                <QueryLoadingPlaceholder query={schemasQuery} />
             </ContentContainer>
         </div>
     );
diff --git a/web/vtadmin/src/components/routes/Tablets.tsx b/web/vtadmin/src/components/routes/Tablets.tsx
index 70c5bdf4c4..b1bf246d53 100644
--- a/web/vtadmin/src/components/routes/Tablets.tsx
+++ b/web/vtadmin/src/components/routes/Tablets.tsx
@@ -37,6 +37,7 @@ import { ShardLink } from '../links/ShardLink';
 import InfoDropdown from './tablets/InfoDropdown';
 import { isReadOnlyMode } from '../../util/env';
 import { ReadOnlyGate } from '../ReadOnlyGate';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
 
 const COLUMNS = ['Keyspace', 'Shard', 'Alias', 'Type', 'Tablet State', 'Hostname'];
 if (!isReadOnlyMode()) {
@@ -47,12 +48,16 @@ export const Tablets = () => {
     useDocumentTitle('Tablets');
 
     const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
-    const { data = [] } = useTablets();
-    const { data: keyspaces = [], ...ksQuery } = useKeyspaces();
+
+    const tabletsQuery = useTablets();
+    const keyspacesQuery = useKeyspaces();
+    const queries = [tabletsQuery, keyspacesQuery];
+
+    const { data: keyspaces = [], ...ksQuery } = keyspacesQuery;
 
     const filteredData = React.useMemo(() => {
-        return formatRows(data, keyspaces, filter);
-    }, [data, filter, keyspaces]);
+        return formatRows(tabletsQuery.data, keyspaces, filter);
+    }, [tabletsQuery.data, filter, keyspaces]);
 
     const renderRows = React.useCallback(
         (rows: typeof filteredData) => {
@@ -119,6 +124,7 @@ export const Tablets = () => {
                     value={filter || ''}
                 />
                 <DataTable columns={COLUMNS} data={filteredData} renderRows={renderRows} />
+                <QueryLoadingPlaceholder queries={queries} />
             </ContentContainer>
         </div>
     );
diff --git a/web/vtadmin/src/components/routes/Vtctlds.tsx b/web/vtadmin/src/components/routes/Vtctlds.tsx
index 331702abda..26efd8498b 100644
--- a/web/vtadmin/src/components/routes/Vtctlds.tsx
+++ b/web/vtadmin/src/components/routes/Vtctlds.tsx
@@ -24,9 +24,12 @@ import { DataTable } from '../dataTable/DataTable';
 import { ContentContainer } from '../layout/ContentContainer';
 import { WorkspaceHeader } from '../layout/WorkspaceHeader';
 import { WorkspaceTitle } from '../layout/WorkspaceTitle';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
 
 export const Vtctlds = () => {
-    const { data: vtctlds = [], ...q } = useVtctlds();
+    const vtctldsQuery = useVtctlds();
+    const { data: vtctlds = [] } = vtctldsQuery;
+
     const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
 
     const data = useMemo(() => {
@@ -81,9 +84,7 @@ export const Vtctlds = () => {
                     value={filter || ''}
                 />
                 <DataTable columns={['Hostname', 'Cluster']} data={data} renderRows={renderRows} />
-
-                {/* TODO skeleton placeholder */}
-                {!!q.isLoading && <div className="text-center">Loading</div>}
+                <QueryLoadingPlaceholder query={vtctldsQuery} />
             </ContentContainer>
         </div>
     );
diff --git a/web/vtadmin/src/components/routes/Workflows.tsx b/web/vtadmin/src/components/routes/Workflows.tsx
index 479056604a..1682e8dc0d 100644
--- a/web/vtadmin/src/components/routes/Workflows.tsx
+++ b/web/vtadmin/src/components/routes/Workflows.tsx
@@ -33,14 +33,17 @@ import { WorkspaceTitle } from '../layout/WorkspaceTitle';
 import { DataFilter } from '../dataTable/DataFilter';
 import { Tooltip } from '../tooltip/Tooltip';
 import { KeyspaceLink } from '../links/KeyspaceLink';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
+import { UseQueryResult } from 'react-query';
 
 export const Workflows = () => {
     useDocumentTitle('Workflows');
-    const { data } = useWorkflows();
+    const workflowsQuery = useWorkflows();
+
     const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
 
     const sortedData = React.useMemo(() => {
-        const mapped = (data || []).map((workflow) => ({
+        const mapped = (workflowsQuery.data || []).map((workflow) => ({
             clusterID: workflow.cluster?.id,
             clusterName: workflow.cluster?.name,
             keyspace: workflow.keyspace,
@@ -54,7 +57,7 @@ export const Workflows = () => {
         }));
         const filtered = filterNouns(filter, mapped);
         return orderBy(filtered, ['name', 'clusterName', 'source', 'target']);
-    }, [data, filter]);
+    }, [workflowsQuery.data, filter]);
 
     const renderRows = (rows: typeof sortedData) =>
         rows.map((row, idx) => {
@@ -150,6 +153,8 @@ export const Workflows = () => {
                     data={sortedData}
                     renderRows={renderRows}
                 />
+
+                <QueryLoadingPlaceholder query={workflowsQuery as UseQueryResult} />
             </ContentContainer>
         </div>
     );
diff --git a/web/vtadmin/src/components/routes/keyspace/Advanced.tsx b/web/vtadmin/src/components/routes/keyspace/Advanced.tsx
new file mode 100644
index 0000000000..b89d180410
--- /dev/null
+++ b/web/vtadmin/src/components/routes/keyspace/Advanced.tsx
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2022 The Vitess Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { UseMutationResult } from 'react-query';
+
+import { useKeyspace, useReloadSchema } from '../../../hooks/api';
+import ActionPanel from '../../ActionPanel';
+import { QueryLoadingPlaceholder } from '../../placeholders/QueryLoadingPlaceholder';
+import { success, warn } from '../../Snackbar';
+
+interface Props {
+    clusterID: string;
+    name: string;
+}
+
+export const Advanced: React.FC<Props> = ({ clusterID, name }) => {
+    const kq = useKeyspace({ clusterID, name });
+
+    const { data: keyspace } = kq;
+
+    const reloadSchemaMutation = useReloadSchema(
+        {
+            clusterIDs: [clusterID],
+            keyspaces: [name],
+        },
+        {
+            onError: (error) => warn(`There was an error reloading the schemas in the ${name} keyspace: ${error}`),
+            onSuccess: () => {
+                success(`Successfully reloaded schemas in the ${name} keyspace.`, { autoClose: 1600 });
+            },
+        }
+    );
+
+    return (
+        <div className="pt-4">
+            <div className="my-8">
+                <h3 className="mb-4">Schemas</h3>
+
+                <QueryLoadingPlaceholder query={kq} />
+
+                {keyspace && (
+                    <div>
+                        <ActionPanel
+                            description={
+                                <>
+                                    Reloads the schema on all the tablets, except the primary tablet, in the{' '}
+                                    <span className="font-bold">{name}</span> keyspace.
+                                </>
+                            }
+                            documentationLink="https://vitess.io/docs/13.0/reference/programs/vtctl/schema-version-permissions/#reloadschemakeyspace"
+                            loadedText="Reload Schema"
+                            loadingText="Reloading Schema..."
+                            mutation={reloadSchemaMutation as UseMutationResult}
+                            title="Reload Schema"
+                        />
+                    </div>
+                )}
+            </div>
+        </div>
+    );
+};
diff --git a/web/vtadmin/src/components/routes/keyspace/Keyspace.tsx b/web/vtadmin/src/components/routes/keyspace/Keyspace.tsx
index 87c33de2f6..61bf52bb18 100644
--- a/web/vtadmin/src/components/routes/keyspace/Keyspace.tsx
+++ b/web/vtadmin/src/components/routes/keyspace/Keyspace.tsx
@@ -18,14 +18,17 @@ import { Link, Redirect, Route } from 'react-router-dom';
 
 import { useKeyspace } from '../../../hooks/api';
 import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
+import { isReadOnlyMode } from '../../../util/env';
 import { Code } from '../../Code';
 import { ContentContainer } from '../../layout/ContentContainer';
 import { NavCrumbs } from '../../layout/NavCrumbs';
 import { WorkspaceHeader } from '../../layout/WorkspaceHeader';
 import { WorkspaceTitle } from '../../layout/WorkspaceTitle';
 import { QueryLoadingPlaceholder } from '../../placeholders/QueryLoadingPlaceholder';
+import { ReadOnlyGate } from '../../ReadOnlyGate';
 import { Tab } from '../../tabs/Tab';
 import { TabContainer } from '../../tabs/TabContainer';
+import { Advanced } from './Advanced';
 import style from './Keyspace.module.scss';
 import { KeyspaceShards } from './KeyspaceShards';
 import { KeyspaceVSchema } from './KeyspaceVSchema';
@@ -91,6 +94,10 @@ export const Keyspace = () => {
                     <Tab text="Shards" to={`${url}/shards`} />
                     <Tab text="VSchema" to={`${url}/vschema`} />
                     <Tab text="JSON" to={`${url}/json`} />
+
+                    <ReadOnlyGate>
+                        <Tab text="Advanced" to={`${url}/advanced`} />
+                    </ReadOnlyGate>
                 </TabContainer>
 
                 <Switch>
@@ -107,6 +114,12 @@ export const Keyspace = () => {
                         <Code code={JSON.stringify(keyspace, null, 2)} />
                     </Route>
 
+                    {!isReadOnlyMode() && (
+                        <Route path={`${path}/advanced`}>
+                            <Advanced clusterID={clusterID} name={name} />
+                        </Route>
+                    )}
+
                     <Redirect exact from={path} to={{ pathname: `${path}/shards`, search }} />
                 </Switch>
             </ContentContainer>
diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts
index a57d7076d8..bad268acac 100644
--- a/web/vtadmin/src/hooks/api.ts
+++ b/web/vtadmin/src/hooks/api.ts
@@ -59,6 +59,7 @@ import {
     validateVersionKeyspace,
     fetchShardReplicationPositions,
     createKeyspace,
+    reloadSchema,
 } from '../api/http';
 import { vtadmin as pb } from '../proto/vtadmin';
 import { formatAlias } from '../util/tablets';
@@ -429,3 +430,16 @@ export const useWorkflow = (
         ...options,
     });
 };
+
+/**
+ * useReloadSchema is a mutate hook that reloads schemas in one or more
+ * keyspaces, shards, or tablets in the cluster, depending on the request parameters.
+ */
+export const useReloadSchema = (
+    params: Parameters<typeof reloadSchema>[0],
+    options?: UseMutationOptions<Awaited<ReturnType<typeof reloadSchema>>, Error>
+) => {
+    return useMutation<Awaited<ReturnType<typeof reloadSchema>>, Error>(() => {
+        return reloadSchema(params);
+    }, options);
+};


Test patch:
diff --git a/go/test/endtoend/onlineddl/vrepl/onlineddl_vrepl_test.go b/go/test/endtoend/onlineddl/vrepl/onlineddl_vrepl_test.go
index 55b3ee8c69..b0a69fcf57 100644
--- a/go/test/endtoend/onlineddl/vrepl/onlineddl_vrepl_test.go
+++ b/go/test/endtoend/onlineddl/vrepl/onlineddl_vrepl_test.go
@@ -603,6 +603,86 @@ func TestSchemaChange(t *testing.T) {
 		onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false)
 		onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false)
 	})
+
+	// Technically the next test should belong in onlineddl_revert suite. But we're tking advantage of setup and functionality existing in this tets:
+	// - two shards as opposed to one
+	// - tablet throttling
+	t.Run("Revert a migration completed on one shard and cancelled on another", func(t *testing.T) {
+		// shard 0 will run normally, shard 1 will be throttled
+		defer unthrottleApp(shards[1].Vttablets[0], throttlerAppName)
+		t.Run("throttle shard 1", func(t *testing.T) {
+			_, body, err := throttleApp(shards[1].Vttablets[0], throttlerAppName)
+			assert.NoError(t, err)
+			assert.Contains(t, body, throttlerAppName)
+		})
+
+		var uuid string
+		t.Run("run migrations, expect 1st to complete, 2nd to be running", func(t *testing.T) {
+			uuid = testOnlineDDLStatement(t, alterTableTrivialStatement, "vitess", providedUUID, providedMigrationContext, "vtgate", "test_val", "", true)
+			{
+				status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards[:1], uuid, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
+				fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
+				onlineddl.CheckMigrationStatus(t, &vtParams, shards[:1], uuid, schema.OnlineDDLStatusComplete)
+			}
+			{
+				// shard 1 is throttled
+				status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards[1:], uuid, normalMigrationWait, schema.OnlineDDLStatusRunning)
+				fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
+				onlineddl.CheckMigrationStatus(t, &vtParams, shards[1:], uuid, schema.OnlineDDLStatusRunning)
+			}
+		})
+		t.Run("check cancel migration", func(t *testing.T) {
+			onlineddl.CheckCancelAllMigrations(t, &vtParams, 1)
+		})
+		t.Run("unthrottle shard 1", func(t *testing.T) {
+			_, body, err := unthrottleApp(shards[1].Vttablets[0], throttlerAppName)
+			assert.NoError(t, err)
+			assert.Contains(t, body, throttlerAppName)
+		})
+		var revertUUID string
+		t.Run("issue revert migration", func(t *testing.T) {
+			revertQuery := fmt.Sprintf("revert vitess_migration '%s'", uuid)
+			rs := onlineddl.VtgateExecQuery(t, &vtParams, revertQuery, "")
+			require.NotNil(t, rs)
+			row := rs.Named().Row()
+			require.NotNil(t, row)
+			revertUUID = row.AsString("uuid", "")
+			assert.NotEmpty(t, revertUUID)
+		})
+		t.Run("expect one revert successful, another failed", func(t *testing.T) {
+			{
+				// shard 0 migration was complete. Revert should be successful
+				status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards[:1], revertUUID, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
+				fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
+				onlineddl.CheckMigrationStatus(t, &vtParams, shards[:1], revertUUID, schema.OnlineDDLStatusComplete)
+			}
+			{
+				// shard 0 migration was cancelled. Revert should not be possible
+				status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards[1:], revertUUID, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
+				fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
+				onlineddl.CheckMigrationStatus(t, &vtParams, shards[1:], revertUUID, schema.OnlineDDLStatusFailed)
+			}
+		})
+		t.Run("expect two rows in SHOW VITESS_MIGRATIONS", func(t *testing.T) {
+			// This validates that the shards are reflected correctly in output of SHOW VITESS_MIGRATIONS
+			rs := onlineddl.ReadMigrations(t, &vtParams, revertUUID)
+			require.NotNil(t, rs)
+			require.Equal(t, 2, len(rs.Rows))
+			for _, row := range rs.Named().Rows {
+				shard := row["shard"].ToString()
+				status := row["migration_status"].ToString()
+
+				switch shard {
+				case "-80":
+					require.Equal(t, string(schema.OnlineDDLStatusComplete), status)
+				case "80-":
+					require.Equal(t, string(schema.OnlineDDLStatusFailed), status)
+				default:
+					require.NoError(t, fmt.Errorf("unexpected shard name: %s", shard))
+				}
+			}
+		})
+	})
 }
 
 func insertRow(t *testing.T) {
diff --git a/go/test/endtoend/onlineddl/vtgate_util.go b/go/test/endtoend/onlineddl/vtgate_util.go
index 4fabee112d..1034fd6f67 100644
--- a/go/test/endtoend/onlineddl/vtgate_util.go
+++ b/go/test/endtoend/onlineddl/vtgate_util.go
@@ -172,6 +172,10 @@ func CheckMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []clu
 
 // WaitForMigrationStatus waits for a migration to reach either provided statuses (returns immediately), or eventually time out
 func WaitForMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, timeout time.Duration, expectStatuses ...schema.OnlineDDLStatus) schema.OnlineDDLStatus {
+	shardNames := map[string]bool{}
+	for _, shard := range shards {
+		shardNames[shard.Name] = true
+	}
 	query, err := sqlparser.ParseAndBind("show vitess_migrations like %a",
 		sqltypes.StringBindVariable(uuid),
 	)
@@ -187,6 +191,11 @@ func WaitForMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []c
 		countMatchedShards := 0
 		r := VtgateExecQuery(t, vtParams, query, "")
 		for _, row := range r.Named().Rows {
+			shardName := row["shard"].ToString()
+			if !shardNames[shardName] {
+				// irrelevant shard
+				continue
+			}
 			lastKnownStatus = row["migration_status"].ToString()
 			if row["migration_uuid"].ToString() == uuid && statusesMap[lastKnownStatus] {
 				countMatchedShards++
diff --git a/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go b/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
index fc5d1915b7..0e20922288 100644
--- a/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
+++ b/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
@@ -120,8 +120,7 @@ func TestEqualFilterOnScatter(t *testing.T) {
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = \"5\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a = 5.00", `[[INT64(5)]]`)
 			mcmp.AssertMatches("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, mcmp.VtConn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) = 5", `expr cannot be translated, not supported`) // will fail since `count(*)` is a FuncExpr
+			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) = 5", `[[INT64(1)]]`)
 		})
 	}
 }
@@ -179,6 +178,9 @@ func TestAggrOnJoin(t *testing.T) {
 
 	mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ a.val1, count(*) as leCount from aggr_test a join t3 t on a.val2 = t.id7 group by a.val1 having leCount = 4",
 		`[[VARCHAR("a") INT64(4)]]`)
+
+	mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ a.val1 from aggr_test a join t3 t on a.val2 = t.id7 group by a.val1 having count(*) = 4",
+		`[[VARCHAR("a")]]`)
 }
 
 func TestNotEqualFilterOnScatter(t *testing.T) {
@@ -200,8 +202,7 @@ func TestNotEqualFilterOnScatter(t *testing.T) {
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != \"1\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != \"5\"", `[]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a != 5.00", `[]`)
-
-			utils.AssertContainsError(t, mcmp.VtConn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) != 5", `expr cannot be translated, not supported`) // will fail since `count(*)` is a FuncExpr
+			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) != 5", `[]`)
 		})
 	}
 }
@@ -224,8 +225,7 @@ func TestLessFilterOnScatter(t *testing.T) {
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < \"10\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < \"5\"", `[]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a < 6.00", `[[INT64(5)]]`)
-
-			utils.AssertContainsError(t, mcmp.VtConn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) < 5", `expr cannot be translated, not supported`) // will fail since `count(*)` is a FuncExpr
+			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) < 5", `[]`)
 		})
 	}
 }
@@ -249,8 +249,7 @@ func TestLessEqualFilterOnScatter(t *testing.T) {
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= \"10\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= \"5\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a <= 5.00", `[[INT64(5)]]`)
-
-			utils.AssertContainsError(t, mcmp.VtConn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) <= 5", `expr cannot be translated, not supported`) // will fail since `count(*)` is a FuncExpr
+			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) <= 5", `[[INT64(1)]]`)
 		})
 	}
 }
@@ -274,8 +273,7 @@ func TestGreaterFilterOnScatter(t *testing.T) {
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > \"1\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > \"5\"", `[]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a > 4.00", `[[INT64(5)]]`)
-
-			utils.AssertContainsError(t, mcmp.VtConn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) > 5", `expr cannot be translated, not supported`) // will fail since `count(*)` is a FuncExpr
+			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) > 5", `[]`)
 		})
 	}
 }
@@ -299,8 +297,7 @@ func TestGreaterEqualFilterOnScatter(t *testing.T) {
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= \"1\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= \"5\"", `[[INT64(5)]]`)
 			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ count(*) as a from aggr_test having a >= 5.00", `[[INT64(5)]]`)
-
-			utils.AssertContainsError(t, mcmp.VtConn, "select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) >= 5", `expr cannot be translated, not supported`) // will fail since `count(*)` is a FuncExpr
+			mcmp.AssertMatches("select /*vt+ PLANNER=gen4 */ 1 from aggr_test having count(*) >= 5", `[[INT64(1)]]`)
 		})
 	}
 }
diff --git a/go/vt/schemadiff/diff_test.go b/go/vt/schemadiff/diff_test.go
index a44f1448f7..8b49a3a9f3 100644
--- a/go/vt/schemadiff/diff_test.go
+++ b/go/vt/schemadiff/diff_test.go
@@ -307,6 +307,28 @@ func TestDiffSchemas(t *testing.T) {
 				"ALTER TABLE `t` MODIFY COLUMN `v` varchar(20)",
 			},
 		},
+		{
+			name: "change of table column tinyint 1 to longer",
+			from: "create table t(id int primary key, i tinyint(1))",
+			to:   "create table t(id int primary key, i tinyint(2))",
+			diffs: []string{
+				"alter table t modify column i tinyint",
+			},
+			cdiffs: []string{
+				"ALTER TABLE `t` MODIFY COLUMN `i` tinyint",
+			},
+		},
+		{
+			name: "change of table column tinyint 2 to 1",
+			from: "create table t(id int primary key, i tinyint(2))",
+			to:   "create table t(id int primary key, i tinyint(1))",
+			diffs: []string{
+				"alter table t modify column i tinyint(1)",
+			},
+			cdiffs: []string{
+				"ALTER TABLE `t` MODIFY COLUMN `i` tinyint(1)",
+			},
+		},
 		{
 			name: "change of table columns, added",
 			from: "create table t(id int primary key)",
diff --git a/go/vt/schemadiff/table_test.go b/go/vt/schemadiff/table_test.go
index b0c0aa9ea6..72c3f86469 100644
--- a/go/vt/schemadiff/table_test.go
+++ b/go/vt/schemadiff/table_test.go
@@ -1193,14 +1193,29 @@ func TestNormalize(t *testing.T) {
 			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` int\n)",
 		},
 		{
-			name: "removes int sizes",
-			from: "create table t (id int primary key, i int(11) default null)",
+			name: "does not remove tinyint(1) size",
+			from: "create table t (id int primary key, i tinyint(1) default null)",
+			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` tinyint(1)\n)",
+		},
+		{
+			name: "removes other tinyint size",
+			from: "create table t (id int primary key, i tinyint(2) default null)",
+			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` tinyint\n)",
+		},
+		{
+			name: "removes int size",
+			from: "create table t (id int primary key, i int(1) default null)",
 			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` int\n)",
 		},
 		{
-			name: "removes zerofill and maps to unsigned",
+			name: "removes bigint size",
+			from: "create table t (id int primary key, i bigint(1) default null)",
+			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` bigint\n)",
+		},
+		{
+			name: "keeps zerofill",
 			from: "create table t (id int primary key, i int zerofill default null)",
-			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` int unsigned\n)",
+			to:   "CREATE TABLE `t` (\n\t`id` int PRIMARY KEY,\n\t`i` int zerofill\n)",
 		},
 		{
 			name: "removes int sizes case insensitive",
diff --git a/go/vt/vtadmin/cluster/cluster_internal_test.go b/go/vt/vtadmin/cluster/cluster_internal_test.go
index 8aea9b29ca..d9e2a19473 100644
--- a/go/vt/vtadmin/cluster/cluster_internal_test.go
+++ b/go/vt/vtadmin/cluster/cluster_internal_test.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"sort"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -51,6 +52,133 @@ type vtctldProxy struct {
 
 func (fake *vtctldProxy) Dial(ctx context.Context) error { return fake.dialErr }
 
+func TestDeleteTablets(t *testing.T) {
+	t.Parallel()
+
+	testClusterProto := &vtadminpb.Cluster{
+		Id:   "test",
+		Name: "test",
+	}
+
+	tests := []struct {
+		name      string
+		cluster   *Cluster
+		timeout   time.Duration
+		setup     func(t testing.TB, c *Cluster)
+		req       *vtctldatapb.DeleteTabletsRequest
+		expected  *vtctldatapb.DeleteTabletsResponse
+		shouldErr bool
+	}{
+		{
+			name: "ok",
+			cluster: &Cluster{
+				Vtctld: &fakevtctldclient.VtctldClient{
+					DeleteTabletsResults: map[string]error{
+						"zone1-0000000100,zone1-0000000101": nil,
+					},
+				},
+				topoRWPool: pools.NewRPCPool(1, time.Millisecond*100, nil),
+			},
+			req: &vtctldatapb.DeleteTabletsRequest{
+				TabletAliases: []*topodatapb.TabletAlias{
+					{
+						Cell: "zone1",
+						Uid:  100,
+					},
+					{
+						Cell: "zone1",
+						Uid:  101,
+					},
+				},
+			},
+			expected: &vtctldatapb.DeleteTabletsResponse{},
+		},
+		{
+			name: "error",
+			cluster: &Cluster{
+				Vtctld:     &fakevtctldclient.VtctldClient{},
+				topoRWPool: pools.NewRPCPool(1, time.Millisecond*100, nil),
+			},
+			req: &vtctldatapb.DeleteTabletsRequest{
+				TabletAliases: []*topodatapb.TabletAlias{
+					{
+						Cell: "zone1",
+						Uid:  100,
+					},
+					{
+						Cell: "zone1",
+						Uid:  101,
+					},
+				},
+			},
+			shouldErr: true,
+		},
+		{
+			name: "RPC pool full",
+			cluster: &Cluster{
+				Vtctld:     &fakevtctldclient.VtctldClient{},
+				topoRWPool: pools.NewRPCPool(1, time.Millisecond*10, nil),
+			},
+			timeout: time.Millisecond * 50,
+			setup: func(t testing.TB, c *Cluster) {
+				err := c.topoRWPool.Acquire(context.Background())
+				require.NoError(t, err, "failed to lock RPC pool")
+				t.Cleanup(c.topoRWPool.Release)
+			},
+			req: &vtctldatapb.DeleteTabletsRequest{
+				TabletAliases: []*topodatapb.TabletAlias{
+					{
+						Cell: "zone1",
+						Uid:  100,
+					},
+					{
+						Cell: "zone1",
+						Uid:  101,
+					},
+				},
+			},
+			shouldErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.cluster.ID = testClusterProto.Id
+			tt.cluster.Name = testClusterProto.Name
+
+			if tt.setup != nil {
+				tt.setup(t, tt.cluster)
+			}
+
+			var (
+				ctx    context.Context
+				cancel context.CancelFunc
+			)
+
+			switch tt.timeout {
+			case 0:
+				ctx, cancel = context.WithCancel(context.Background())
+			default:
+				ctx, cancel = context.WithTimeout(context.Background(), tt.timeout)
+			}
+			defer cancel()
+
+			resp, err := tt.cluster.DeleteTablets(ctx, tt.req)
+			if tt.shouldErr {
+				assert.Error(t, err, "expected error, got %+v", resp)
+				return
+			}
+
+			require.NoError(t, err)
+			utils.MustMatch(t, tt.expected, resp)
+		})
+	}
+}
+
 func Test_getShardSets(t *testing.T) {
 	t.Parallel()
 
@@ -194,6 +322,114 @@ func Test_getShardSets(t *testing.T) {
 	}
 }
 
+func TestRefreshState(t *testing.T) {
+	t.Parallel()
+
+	testClusterProto := &vtadminpb.Cluster{
+		Id:   "test",
+		Name: "test",
+	}
+
+	tests := []struct {
+		name              string
+		cluster           *Cluster
+		timeout           time.Duration
+		setup             func(t testing.TB, c *Cluster)
+		tablet            *vtadminpb.Tablet
+		assertion         func(t assert.TestingT, err error, msgAndArgs ...any) bool
+		assertionMsgExtra []any
+	}{
+		{
+			name: "ok",
+			cluster: &Cluster{
+				Vtctld: &fakevtctldclient.VtctldClient{
+					RefreshStateResults: map[string]error{
+						"zone1-0000000100": nil,
+					},
+				},
+				topoReadPool: pools.NewRPCPool(1, time.Millisecond*100, nil),
+			},
+			tablet: &vtadminpb.Tablet{
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			assertion: assert.NoError,
+		},
+		{
+			name: "error",
+			cluster: &Cluster{
+				Vtctld: &fakevtctldclient.VtctldClient{
+					RefreshStateResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+					},
+				},
+				topoReadPool: pools.NewRPCPool(1, time.Millisecond*100, nil),
+			},
+			tablet: &vtadminpb.Tablet{
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			assertion: assert.Error,
+		},
+		{
+			name: "RPC pool full",
+			cluster: &Cluster{
+				Vtctld:       &fakevtctldclient.VtctldClient{},
+				topoReadPool: pools.NewRPCPool(1, time.Millisecond*10, nil),
+			},
+			timeout: time.Millisecond * 50,
+			setup: func(t testing.TB, c *Cluster) {
+				err := c.topoReadPool.Acquire(context.Background())
+				require.NoError(t, err, "failed to lock RPC pool")
+				t.Cleanup(c.topoReadPool.Release)
+			},
+			tablet: &vtadminpb.Tablet{
+				Tablet: &topodatapb.Tablet{Alias: &topodatapb.TabletAlias{}},
+			},
+			assertion: assert.Error,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.cluster.ID = testClusterProto.Id
+			tt.cluster.Name = testClusterProto.Name
+
+			if tt.setup != nil {
+				tt.setup(t, tt.cluster)
+			}
+
+			var (
+				ctx    context.Context
+				cancel context.CancelFunc
+			)
+
+			switch tt.timeout {
+			case 0:
+				ctx, cancel = context.WithCancel(context.Background())
+			default:
+				ctx, cancel = context.WithTimeout(context.Background(), tt.timeout)
+			}
+			defer cancel()
+
+			err := tt.cluster.RefreshState(ctx, tt.tablet)
+			tt.assertion(t, err, tt.assertionMsgExtra...)
+		})
+	}
+}
+
 func Test_reloadKeyspaceSchemas(t *testing.T) {
 	t.Parallel()
 
@@ -996,3 +1232,133 @@ func Test_reloadTabletSchemas(t *testing.T) {
 		})
 	}
 }
+
+func TestReparentTablet(t *testing.T) {
+	t.Parallel()
+
+	testClusterProto := &vtadminpb.Cluster{
+		Id:   "test",
+		Name: "test",
+	}
+
+	tests := []struct {
+		name      string
+		cluster   *Cluster
+		timeout   time.Duration
+		setup     func(t testing.TB, c *Cluster)
+		tablet    *vtadminpb.Tablet
+		expected  *vtadminpb.ReparentTabletResponse
+		shouldErr bool
+	}{
+		{
+			name: "ok",
+			cluster: &Cluster{
+				Vtctld: &fakevtctldclient.VtctldClient{
+					ReparentTabletResults: map[string]struct {
+						Response *vtctldatapb.ReparentTabletResponse
+						Error    error
+					}{
+						"zone1-0000000100": {
+							Response: &vtctldatapb.ReparentTabletResponse{
+								Keyspace: "testks",
+								Shard:    "-",
+								Primary: &topodatapb.TabletAlias{
+									Cell: "zone1",
+									Uid:  500,
+								},
+							},
+						},
+					},
+				},
+				topoRWPool: pools.NewRPCPool(1, time.Millisecond*100, nil),
+			},
+			tablet: &vtadminpb.Tablet{
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			expected: &vtadminpb.ReparentTabletResponse{
+				Keyspace: "testks",
+				Shard:    "-",
+				Primary: &topodatapb.TabletAlias{
+					Cell: "zone1",
+					Uid:  500,
+				},
+				Cluster: testClusterProto,
+			},
+		},
+		{
+			name: "error",
+			cluster: &Cluster{
+				Vtctld:     &fakevtctldclient.VtctldClient{},
+				topoRWPool: pools.NewRPCPool(1, time.Millisecond*100, nil),
+			},
+			tablet: &vtadminpb.Tablet{
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			shouldErr: true,
+		},
+		{
+			name: "RPC pool full",
+			cluster: &Cluster{
+				Vtctld:     &fakevtctldclient.VtctldClient{},
+				topoRWPool: pools.NewRPCPool(1, time.Millisecond*10, nil),
+			},
+			timeout: time.Millisecond * 50,
+			setup: func(t testing.TB, c *Cluster) {
+				err := c.topoRWPool.Acquire(context.Background())
+				require.NoError(t, err, "failed to lock RPC pool")
+				t.Cleanup(c.topoRWPool.Release)
+			},
+			tablet: &vtadminpb.Tablet{
+				Tablet: &topodatapb.Tablet{Alias: &topodatapb.TabletAlias{}},
+			},
+			shouldErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.cluster.ID = testClusterProto.Id
+			tt.cluster.Name = testClusterProto.Name
+
+			if tt.setup != nil {
+				tt.setup(t, tt.cluster)
+			}
+
+			var (
+				ctx    context.Context
+				cancel context.CancelFunc
+			)
+
+			switch tt.timeout {
+			case 0:
+				ctx, cancel = context.WithCancel(context.Background())
+			default:
+				ctx, cancel = context.WithTimeout(context.Background(), tt.timeout)
+			}
+			defer cancel()
+
+			resp, err := tt.cluster.ReparentTablet(ctx, tt.tablet)
+			if tt.shouldErr {
+				assert.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			utils.MustMatch(t, tt.expected, resp)
+		})
+	}
+}
diff --git a/go/vt/vtadmin/cluster/cluster_test.go b/go/vt/vtadmin/cluster/cluster_test.go
index 599f0ba076..e388ae1658 100644
--- a/go/vt/vtadmin/cluster/cluster_test.go
+++ b/go/vt/vtadmin/cluster/cluster_test.go
@@ -3365,3 +3365,219 @@ func TestGetWorkflows(t *testing.T) {
 		})
 	}
 }
+
+func TestSetWritable(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	tests := []struct {
+		name              string
+		cfg               testutil.TestClusterConfig
+		req               *vtctldatapb.SetWritableRequest
+		assertion         func(t assert.TestingT, err error, msgAndArgs ...any) bool
+		assertionMsgExtra []any
+	}{
+		{
+			name: "ok",
+			cfg: testutil.TestClusterConfig{
+				Cluster: &vtadminpb.Cluster{
+					Id:   "test",
+					Name: "test",
+				},
+				VtctldClient: &fakevtctldclient.VtctldClient{
+					SetWritableResults: map[string]error{
+						"zone1-0000000100": nil,
+					},
+				},
+			},
+			req: &vtctldatapb.SetWritableRequest{
+				TabletAlias: &topodatapb.TabletAlias{
+					Cell: "zone1",
+					Uid:  100,
+				},
+				Writable: true,
+			},
+			assertion: assert.NoError,
+		},
+		{
+			name: "error",
+			cfg: testutil.TestClusterConfig{
+				Cluster: &vtadminpb.Cluster{
+					Id:   "test",
+					Name: "test",
+				},
+				VtctldClient: &fakevtctldclient.VtctldClient{
+					SetWritableResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+					},
+				},
+			},
+			req: &vtctldatapb.SetWritableRequest{
+				TabletAlias: &topodatapb.TabletAlias{
+					Cell: "zone1",
+					Uid:  100,
+				},
+				Writable: true,
+			},
+			assertion: assert.Error,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			c := testutil.BuildCluster(t, tt.cfg)
+			err := c.SetWritable(ctx, tt.req)
+			tt.assertion(t, err, tt.assertionMsgExtra...)
+		})
+	}
+}
+
+func TestToggleTabletReplication(t *testing.T) {
+	t.Parallel()
+
+	type ReplicationState bool
+	const (
+		Start ReplicationState = true
+		Stop  ReplicationState = false
+	)
+
+	testClusterProto := &vtadminpb.Cluster{
+		Id:   "test",
+		Name: "test",
+	}
+
+	ctx := context.Background()
+	tests := []struct {
+		name              string
+		cfg               testutil.TestClusterConfig
+		tablet            *vtadminpb.Tablet
+		state             ReplicationState
+		assertion         func(t assert.TestingT, err error, msgAndArgs ...any) bool
+		assertionMsgExtra []any
+	}{
+		{
+			name: "start/ok",
+			cfg: testutil.TestClusterConfig{
+				Cluster: testClusterProto,
+				VtctldClient: &fakevtctldclient.VtctldClient{
+					StartReplicationResults: map[string]error{
+						"zone1-0000000100": nil,
+						"zone1-0000000101": fmt.Errorf("some error"),
+					},
+					StopReplicationResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+						"zone1-0000000101": nil,
+					},
+				},
+			},
+			tablet: &vtadminpb.Tablet{
+				Cluster: testClusterProto,
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			state:     Start,
+			assertion: assert.NoError,
+		},
+		{
+			name: "start/error",
+			cfg: testutil.TestClusterConfig{
+				Cluster: testClusterProto,
+				VtctldClient: &fakevtctldclient.VtctldClient{
+					StartReplicationResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+						"zone1-0000000101": nil,
+					},
+					StopReplicationResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+						"zone1-0000000101": nil,
+					},
+				},
+			},
+			tablet: &vtadminpb.Tablet{
+				Cluster: testClusterProto,
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			state:     Start,
+			assertion: assert.Error,
+		},
+		{
+			name: "stop/ok",
+			cfg: testutil.TestClusterConfig{
+				Cluster: testClusterProto,
+				VtctldClient: &fakevtctldclient.VtctldClient{
+					StartReplicationResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+						"zone1-0000000101": nil,
+					},
+					StopReplicationResults: map[string]error{
+						"zone1-0000000100": nil,
+						"zone1-0000000101": fmt.Errorf("some error"),
+					},
+				},
+			},
+			tablet: &vtadminpb.Tablet{
+				Cluster: testClusterProto,
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			state:     Stop,
+			assertion: assert.NoError,
+		},
+		{
+			name: "stop/error",
+			cfg: testutil.TestClusterConfig{
+				Cluster: testClusterProto,
+				VtctldClient: &fakevtctldclient.VtctldClient{
+					StartReplicationResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+						"zone1-0000000101": nil,
+					},
+					StopReplicationResults: map[string]error{
+						"zone1-0000000100": fmt.Errorf("some error"),
+						"zone1-0000000101": nil,
+					},
+				},
+			},
+			tablet: &vtadminpb.Tablet{
+				Cluster: testClusterProto,
+				Tablet: &topodatapb.Tablet{
+					Alias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  100,
+					},
+				},
+			},
+			state:     Stop,
+			assertion: assert.Error,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			c := testutil.BuildCluster(t, tt.cfg)
+			err := c.ToggleTabletReplication(ctx, tt.tablet, bool(tt.state))
+			tt.assertion(t, err, tt.assertionMsgExtra...)
+		})
+	}
+}
diff --git a/go/vt/vtgate/engine/projection_test.go b/go/vt/vtgate/engine/projection_test.go
index 09bc9ff3e4..42bc7555ed 100644
--- a/go/vt/vtgate/engine/projection_test.go
+++ b/go/vt/vtgate/engine/projection_test.go
@@ -32,8 +32,8 @@ import (
 func TestMultiply(t *testing.T) {
 	expr := &sqlparser.BinaryExpr{
 		Operator: sqlparser.MultOp,
-		Left:     sqlparser.Offset(0),
-		Right:    sqlparser.Offset(1),
+		Left:     &sqlparser.Offset{V: 0},
+		Right:    &sqlparser.Offset{V: 1},
 	}
 	evalExpr, err := evalengine.Translate(expr, nil)
 	require.NoError(t, err)
diff --git a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
index f08c0f2e72..a8ca9c018d 100644
--- a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
@@ -2112,7 +2112,35 @@ Gen4 plan same as above
     ]
   }
 }
-Gen4 error: [BUG] unable to plan distinct query as the column is not projected: a
+{
+  "QueryType": "SELECT",
+  "Original": "select distinct a, count(*) from user",
+  "Instructions": {
+    "OperatorType": "Aggregate",
+    "Variant": "Ordered",
+    "GroupBy": "0, 1",
+    "Inputs": [
+      {
+        "OperatorType": "Aggregate",
+        "Variant": "Scalar",
+        "Aggregates": "random(0) AS a, sum(1) AS count(*)",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "Scatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select a, count(*) from `user` where 1 != 1",
+            "Query": "select a, count(*) from `user`",
+            "Table": "`user`"
+          }
+        ]
+      }
+    ]
+  }
+}
 
 # distinct and aggregate functions
 "select distinct a, count(*) from user group by a"
@@ -3243,7 +3271,46 @@ Gen4 plan same as above
     "Vindex": "name_user_map"
   }
 }
-Gen4 error: cannot push projections in ordered aggregates
+{
+  "QueryType": "SELECT",
+  "Original": "select 1 from user having count(id) = 10 and name = 'a'",
+  "Instructions": {
+    "OperatorType": "SimpleProjection",
+    "Columns": [
+      0
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Filter",
+        "Predicate": "OFFSET(1, 'count(id)') = 10",
+        "Inputs": [
+          {
+            "OperatorType": "Aggregate",
+            "Variant": "Scalar",
+            "Aggregates": "random(0) AS 1, sum(1) AS count(id)",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "Equal",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select 1, count(id) from `user` where 1 != 1",
+                "Query": "select 1, count(id) from `user` where `name` = 'a'",
+                "Table": "`user`",
+                "Values": [
+                  "VARCHAR(\"a\")"
+                ],
+                "Vindex": "name_user_map"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
 
 # Aggregates and joins
 "select count(*) from user join user_extra"
@@ -3315,7 +3382,42 @@ Gen4 error: cannot push projections in ordered aggregates
     "Table": "`user`"
   }
 }
-Gen4 error: cannot push projections in ordered aggregates
+{
+  "QueryType": "SELECT",
+  "Original": "select 1 from user having count(id) = 10",
+  "Instructions": {
+    "OperatorType": "SimpleProjection",
+    "Columns": [
+      0
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Filter",
+        "Predicate": "OFFSET(1, 'count(id)') = 10",
+        "Inputs": [
+          {
+            "OperatorType": "Aggregate",
+            "Variant": "Scalar",
+            "Aggregates": "random(0) AS 1, sum(1) AS count(id)",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "Scatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select 1, count(id) from `user` where 1 != 1",
+                "Query": "select 1, count(id) from `user`",
+                "Table": "`user`"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
 
 # Aggregate on join
 "select user.a, count(*) from user join user_extra group by user.a"
@@ -4056,7 +4158,7 @@ Gen4 error: aggregate functions take a single argument 'count(distinct user_id,
     "Inputs": [
       {
         "OperatorType": "Filter",
-        "Predicate": "[1] = 3",
+        "Predicate": "OFFSET(1, 'count(*)') = 3",
         "Inputs": [
           {
             "OperatorType": "Aggregate",
@@ -4100,7 +4202,7 @@ Gen4 error: aggregate functions take a single argument 'count(distinct user_id,
     "Inputs": [
       {
         "OperatorType": "Filter",
-        "Predicate": "[1] + [2] = 42",
+        "Predicate": "OFFSET(1, 'sum(foo)') + OFFSET(2, 'sum(bar)') = 42",
         "Inputs": [
           {
             "OperatorType": "Aggregate",
@@ -4144,7 +4246,7 @@ Gen4 error: aggregate functions take a single argument 'count(distinct user_id,
     "Inputs": [
       {
         "OperatorType": "Filter",
-        "Predicate": "fooSum + [2] = 42",
+        "Predicate": "fooSum + OFFSET(2, 'sum(bar)') = 42",
         "Inputs": [
           {
             "OperatorType": "Aggregate",
@@ -4171,3 +4273,498 @@ Gen4 error: aggregate functions take a single argument 'count(distinct user_id,
     ]
   }
 }
+
+# having should be able to add new aggregation expressions in having
+"select foo from user group by foo having count(*) = 3"
+"unsupported: filtering on results of aggregates"
+{
+  "QueryType": "SELECT",
+  "Original": "select foo from user group by foo having count(*) = 3",
+  "Instructions": {
+    "OperatorType": "SimpleProjection",
+    "Columns": [
+      0
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Filter",
+        "Predicate": "OFFSET(1, 'count(*)') = 3",
+        "Inputs": [
+          {
+            "OperatorType": "Aggregate",
+            "Variant": "Ordered",
+            "Aggregates": "sum(1) AS count(*)",
+            "GroupBy": "(0|2)",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "Scatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select foo, count(*), weight_string(foo) from `user` where 1 != 1 group by foo, weight_string(foo)",
+                "OrderBy": "(0|2) ASC",
+                "Query": "select foo, count(*), weight_string(foo) from `user` group by foo, weight_string(foo) order by foo asc",
+                "Table": "`user`"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+"select u.id from user u join user_extra ue on ue.id = u.id group by u.id having count(u.name) = 3"
+"unsupported: cross-shard query with aggregates"
+{
+  "QueryType": "SELECT",
+  "Original": "select u.id from user u join user_extra ue on ue.id = u.id group by u.id having count(u.name) = 3",
+  "Instructions": {
+    "OperatorType": "SimpleProjection",
+    "Columns": [
+      0
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Filter",
+        "Predicate": "OFFSET(1, 'count(u.`name`)') = 3",
+        "Inputs": [
+          {
+            "OperatorType": "Aggregate",
+            "Variant": "Ordered",
+            "Aggregates": "sum(1) AS count(u.`name`)",
+            "GroupBy": "(0|2)",
+            "Inputs": [
+              {
+                "OperatorType": "Projection",
+                "Expressions": [
+                  "[COLUMN 0] as id",
+                  "[COLUMN 2] * [COLUMN 3] as count(u.`name`)",
+                  "[COLUMN 1]"
+                ],
+                "Inputs": [
+                  {
+                    "OperatorType": "Sort",
+                    "Variant": "Memory",
+                    "OrderBy": "(0|1) ASC",
+                    "Inputs": [
+                      {
+                        "OperatorType": "Join",
+                        "Variant": "Join",
+                        "JoinColumnIndexes": "R:1,R:2,L:1,R:0",
+                        "JoinVars": {
+                          "ue_id": 0
+                        },
+                        "TableName": "user_extra_`user`",
+                        "Inputs": [
+                          {
+                            "OperatorType": "Route",
+                            "Variant": "Scatter",
+                            "Keyspace": {
+                              "Name": "user",
+                              "Sharded": true
+                            },
+                            "FieldQuery": "select ue.id, count(*), weight_string(ue.id) from user_extra as ue where 1 != 1 group by ue.id, weight_string(ue.id)",
+                            "Query": "select ue.id, count(*), weight_string(ue.id) from user_extra as ue group by ue.id, weight_string(ue.id)",
+                            "Table": "user_extra"
+                          },
+                          {
+                            "OperatorType": "Route",
+                            "Variant": "EqualUnique",
+                            "Keyspace": {
+                              "Name": "user",
+                              "Sharded": true
+                            },
+                            "FieldQuery": "select count(u.`name`), u.id, weight_string(u.id) from `user` as u where 1 != 1 group by u.id, weight_string(u.id)",
+                            "Query": "select count(u.`name`), u.id, weight_string(u.id) from `user` as u where u.id = :ue_id group by u.id, weight_string(u.id)",
+                            "Table": "`user`",
+                            "Values": [
+                              ":ue_id"
+                            ],
+                            "Vindex": "user_index"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+"select u.id from user u join user_extra ue on ue.user_id = u.id group by u.id having count(u.name) = 3"
+{
+  "QueryType": "SELECT",
+  "Original": "select u.id from user u join user_extra ue on ue.user_id = u.id group by u.id having count(u.name) = 3",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "Scatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select u.id from `user` as u join user_extra as ue on ue.user_id = u.id where 1 != 1 group by u.id",
+    "Query": "select u.id from `user` as u join user_extra as ue on ue.user_id = u.id group by u.id having count(u.`name`) = 3",
+    "Table": "`user`, user_extra"
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select u.id from user u join user_extra ue on ue.user_id = u.id group by u.id having count(u.name) = 3",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "Scatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select u.id from `user` as u, user_extra as ue where 1 != 1 group by u.id",
+    "Query": "select u.id from `user` as u, user_extra as ue where ue.user_id = u.id group by u.id having count(u.`name`) = 3",
+    "Table": "`user`, user_extra"
+  }
+}
+
+# only extract the aggregation once, even if used twice
+"select u.id from user u join user_extra ue on ue.id = u.id group by u.id having count(*) < 3 and count(*) > 5"
+"unsupported: cross-shard query with aggregates"
+{
+  "QueryType": "SELECT",
+  "Original": "select u.id from user u join user_extra ue on ue.id = u.id group by u.id having count(*) \u003c 3 and count(*) \u003e 5",
+  "Instructions": {
+    "OperatorType": "SimpleProjection",
+    "Columns": [
+      0
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Filter",
+        "Predicate": "OFFSET(1, 'count(*)') \u003c 3 and OFFSET(1, 'count(*)') \u003e 5",
+        "Inputs": [
+          {
+            "OperatorType": "Aggregate",
+            "Variant": "Ordered",
+            "Aggregates": "sum(1) AS count(*)",
+            "GroupBy": "(0|2)",
+            "Inputs": [
+              {
+                "OperatorType": "Projection",
+                "Expressions": [
+                  "[COLUMN 0] as id",
+                  "[COLUMN 2] * [COLUMN 3] as count(*)",
+                  "[COLUMN 1]"
+                ],
+                "Inputs": [
+                  {
+                    "OperatorType": "Sort",
+                    "Variant": "Memory",
+                    "OrderBy": "(0|1) ASC",
+                    "Inputs": [
+                      {
+                        "OperatorType": "Join",
+                        "Variant": "Join",
+                        "JoinColumnIndexes": "R:1,R:2,L:1,R:0",
+                        "JoinVars": {
+                          "ue_id": 0
+                        },
+                        "TableName": "user_extra_`user`",
+                        "Inputs": [
+                          {
+                            "OperatorType": "Route",
+                            "Variant": "Scatter",
+                            "Keyspace": {
+                              "Name": "user",
+                              "Sharded": true
+                            },
+                            "FieldQuery": "select ue.id, count(*), weight_string(ue.id) from user_extra as ue where 1 != 1 group by ue.id, weight_string(ue.id)",
+                            "Query": "select ue.id, count(*), weight_string(ue.id) from user_extra as ue group by ue.id, weight_string(ue.id)",
+                            "Table": "user_extra"
+                          },
+                          {
+                            "OperatorType": "Route",
+                            "Variant": "EqualUnique",
+                            "Keyspace": {
+                              "Name": "user",
+                              "Sharded": true
+                            },
+                            "FieldQuery": "select count(*), u.id, weight_string(u.id) from `user` as u where 1 != 1 group by u.id, weight_string(u.id)",
+                            "Query": "select count(*), u.id, weight_string(u.id) from `user` as u where u.id = :ue_id group by u.id, weight_string(u.id)",
+                            "Table": "`user`",
+                            "Values": [
+                              ":ue_id"
+                            ],
+                            "Vindex": "user_index"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+"select (select 1 from user u having count(ue.col) > 10) from user_extra ue"
+"symbol ue.col not found in subquery"
+{
+  "QueryType": "SELECT",
+  "Original": "select (select 1 from user u having count(ue.col) \u003e 10) from user_extra ue",
+  "Instructions": {
+    "OperatorType": "Subquery",
+    "Variant": "PulloutValue",
+    "PulloutVars": [
+      "__sq1"
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "SimpleProjection",
+        "Columns": [
+          0
+        ],
+        "Inputs": [
+          {
+            "OperatorType": "Filter",
+            "Predicate": "OFFSET(1, 'count(ue.col)') \u003e 10",
+            "Inputs": [
+              {
+                "OperatorType": "Aggregate",
+                "Variant": "Scalar",
+                "Aggregates": "random(0) AS 1, sum(1) AS count(ue.col)",
+                "Inputs": [
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "Scatter",
+                    "Keyspace": {
+                      "Name": "user",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select 1, count(ue.col) from `user` as u where 1 != 1",
+                    "Query": "select 1, count(ue.col) from `user` as u",
+                    "Table": "`user`"
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "Scatter",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select :__sq1 from user_extra as ue where 1 != 1",
+        "Query": "select :__sq1 from user_extra as ue",
+        "Table": "user_extra"
+      }
+    ]
+  }
+}
+
+# group by and ',' joins with condition
+"select user.col from user join user_extra on user_extra.col = user.col group by user.id"
+"unsupported: cross-shard query with aggregates"
+{
+  "QueryType": "SELECT",
+  "Original": "select user.col from user join user_extra on user_extra.col = user.col group by user.id",
+  "Instructions": {
+    "OperatorType": "Aggregate",
+    "Variant": "Ordered",
+    "Aggregates": "random(0) AS col",
+    "GroupBy": "(2|1)",
+    "ResultColumns": 1,
+    "Inputs": [
+      {
+        "OperatorType": "Projection",
+        "Expressions": [
+          "[COLUMN 2] * [COLUMN 3] as col",
+          "[COLUMN 1]",
+          "[COLUMN 0] as id"
+        ],
+        "Inputs": [
+          {
+            "OperatorType": "Join",
+            "Variant": "Join",
+            "JoinColumnIndexes": "L:1,L:2,L:0,R:0",
+            "JoinVars": {
+              "user_col": 0
+            },
+            "TableName": "`user`_user_extra",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "Scatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select `user`.col, `user`.id, weight_string(`user`.id) from `user` where 1 != 1 group by `user`.col, `user`.id, weight_string(`user`.id)",
+                "OrderBy": "(1|2) ASC",
+                "Query": "select `user`.col, `user`.id, weight_string(`user`.id) from `user` group by `user`.col, `user`.id, weight_string(`user`.id) order by `user`.id asc",
+                "Table": "`user`"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "Scatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select count(*) from user_extra where 1 != 1",
+                "Query": "select count(*) from user_extra where user_extra.col = :user_col",
+                "Table": "user_extra"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+
+# scatter aggregate symtab lookup error
+"select id, b as id, count(*) from user order by id"
+"ambiguous symbol reference: id"
+{
+  "QueryType": "SELECT",
+  "Original": "select id, b as id, count(*) from user order by id",
+  "Instructions": {
+    "OperatorType": "Aggregate",
+    "Variant": "Scalar",
+    "Aggregates": "random(0) AS id, random(1) AS id, sum(2) AS count(*)",
+    "ResultColumns": 3,
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "Scatter",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id, b as id, count(*), weight_string(b) from `user` where 1 != 1",
+        "OrderBy": "(1|3) ASC",
+        "Query": "select id, b as id, count(*), weight_string(b) from `user` order by id asc",
+        "Table": "`user`"
+      }
+    ]
+  }
+}
+
+# aggr and non-aggr without group by (with query does not give useful result out)
+"select id, count(*) from user"
+{
+  "QueryType": "SELECT",
+  "Original": "select id, count(*) from user",
+  "Instructions": {
+    "OperatorType": "Aggregate",
+    "Variant": "Scalar",
+    "Aggregates": "count(1)",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "Scatter",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id, count(*) from `user` where 1 != 1",
+        "Query": "select id, count(*) from `user`",
+        "Table": "`user`"
+      }
+    ]
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select id, count(*) from user",
+  "Instructions": {
+    "OperatorType": "Aggregate",
+    "Variant": "Scalar",
+    "Aggregates": "random(0) AS id, sum(1) AS count(*)",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "Scatter",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id, count(*) from `user` where 1 != 1",
+        "Query": "select id, count(*) from `user`",
+        "Table": "`user`"
+      }
+    ]
+  }
+}
+
+# group by and ',' joins
+"select user.id from user, user_extra group by id"
+"unsupported: cross-shard query with aggregates"
+{
+  "QueryType": "SELECT",
+  "Original": "select user.id from user, user_extra group by id",
+  "Instructions": {
+    "OperatorType": "Aggregate",
+    "Variant": "Ordered",
+    "Aggregates": "random(0) AS id",
+    "GroupBy": "(2|1)",
+    "ResultColumns": 1,
+    "Inputs": [
+      {
+        "OperatorType": "Projection",
+        "Expressions": [
+          "[COLUMN 2] * [COLUMN 3] as id",
+          "[COLUMN 1]",
+          "[COLUMN 0] as id"
+        ],
+        "Inputs": [
+          {
+            "OperatorType": "Join",
+            "Variant": "Join",
+            "JoinColumnIndexes": "L:0,L:1,L:0,R:0",
+            "TableName": "`user`_user_extra",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "Scatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select `user`.id, weight_string(id) from `user` where 1 != 1 group by id, weight_string(id)",
+                "OrderBy": "(0|1) ASC",
+                "Query": "select `user`.id, weight_string(id) from `user` group by id, weight_string(id) order by id asc",
+                "Table": "`user`"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "Scatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select count(*) from user_extra where 1 != 1",
+                "Query": "select count(*) from user_extra",
+                "Table": "user_extra"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/go/vt/vtgate/planbuilder/testdata/systemtables_cases.txt b/go/vt/vtgate/planbuilder/testdata/systemtables_cases.txt
index 86029491aa..465354d28f 100644
--- a/go/vt/vtgate/planbuilder/testdata/systemtables_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/systemtables_cases.txt
@@ -1431,3 +1431,23 @@ Gen4 plan same as above
     "Table": "information_schema.key_column_usage"
   }
 }
+
+# table_schema OR predicate
+# It is unsupported because we do not route queries to multiple keyspaces right now
+"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'ks' OR TABLE_SCHEMA = 'main'"
+{
+  "QueryType": "SELECT",
+  "Original": "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'ks' OR TABLE_SCHEMA = 'main'",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "DBA",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": false
+    },
+    "FieldQuery": "select * from INFORMATION_SCHEMA.`TABLES` where 1 != 1",
+    "Query": "select * from INFORMATION_SCHEMA.`TABLES` where TABLE_SCHEMA = 'ks' or TABLE_SCHEMA = 'main'",
+    "Table": "INFORMATION_SCHEMA.`TABLES`"
+  }
+}
+Gen4 plan same as above
diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
index 7e84405e17..4124d264a2 100644
--- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
@@ -68,35 +68,6 @@ Gen4 plan same as above
 "unsupported: '*' expression in cross-shard query"
 Gen4 error: cannot use column offsets in group statement when using `*`
 
-# group by must reference select list
-"select a from user group by b"
-"unsupported: in scatter query: group by column must reference column in SELECT list"
-{
-  "QueryType": "SELECT",
-  "Original": "select a from user group by b",
-  "Instructions": {
-    "OperatorType": "Aggregate",
-    "Variant": "Ordered",
-    "Aggregates": "random(0) AS a",
-    "GroupBy": "(1|2)",
-    "ResultColumns": 1,
-    "Inputs": [
-      {
-        "OperatorType": "Route",
-        "Variant": "Scatter",
-        "Keyspace": {
-          "Name": "user",
-          "Sharded": true
-        },
-        "FieldQuery": "select a, b, weight_string(b) from `user` where 1 != 1 group by b, weight_string(b)",
-        "OrderBy": "(1|2) ASC",
-        "Query": "select a, b, weight_string(b) from `user` group by b, weight_string(b) order by b asc",
-        "Table": "`user`"
-      }
-    ]
-  }
-}
-
 # complex group by expression
 "select a from user group by a+1"
 "unsupported: in scatter query: only simple references allowed"
@@ -112,34 +83,6 @@ Gen4 plan same as above
 "unsupported: only one expression allowed inside aggregates: count(a, b)"
 Gen4 error: aggregate functions take a single argument 'count(a, b)'
 
-# scatter aggregate symtab lookup error
-"select id, b as id, count(*) from user order by id"
-"ambiguous symbol reference: id"
-{
-  "QueryType": "SELECT",
-  "Original": "select id, b as id, count(*) from user order by id",
-  "Instructions": {
-    "OperatorType": "Aggregate",
-    "Variant": "Scalar",
-    "Aggregates": "random(0) AS id, random(0) AS id, sum(2) AS count(*)",
-    "ResultColumns": 3,
-    "Inputs": [
-      {
-        "OperatorType": "Route",
-        "Variant": "Scatter",
-        "Keyspace": {
-          "Name": "user",
-          "Sharded": true
-        },
-        "FieldQuery": "select id, b as id, count(*), weight_string(b) from `user` where 1 != 1",
-        "OrderBy": "(1|3) ASC",
-        "Query": "select id, b as id, count(*), weight_string(b) from `user` order by id asc",
-        "Table": "`user`"
-      }
-    ]
-  }
-}
-
 # scatter aggregate complex order by
 "select id from user group by id order by id+1"
 "unsupported: in scatter query: complex order by expression: id + 1"
@@ -155,125 +98,6 @@ Gen4 plan same as above
 "unsupported: cross-shard query with aggregates"
 Gen4 error: unsupported: in scatter query: complex aggregate expression
 
-# group by and ',' joins
-"select user.id from user, user_extra group by id"
-"unsupported: cross-shard query with aggregates"
-{
-  "QueryType": "SELECT",
-  "Original": "select user.id from user, user_extra group by id",
-  "Instructions": {
-    "OperatorType": "Aggregate",
-    "Variant": "Ordered",
-    "Aggregates": "random(0) AS id",
-    "GroupBy": "(2|1)",
-    "ResultColumns": 1,
-    "Inputs": [
-      {
-        "OperatorType": "Projection",
-        "Expressions": [
-          "[COLUMN 2] * [COLUMN 3] as id",
-          "[COLUMN 1]",
-          "[COLUMN 0] as id"
-        ],
-        "Inputs": [
-          {
-            "OperatorType": "Join",
-            "Variant": "Join",
-            "JoinColumnIndexes": "L:0,L:1,L:0,R:0",
-            "TableName": "`user`_user_extra",
-            "Inputs": [
-              {
-                "OperatorType": "Route",
-                "Variant": "Scatter",
-                "Keyspace": {
-                  "Name": "user",
-                  "Sharded": true
-                },
-                "FieldQuery": "select `user`.id, weight_string(id) from `user` where 1 != 1 group by id, weight_string(id)",
-                "OrderBy": "(0|1) ASC",
-                "Query": "select `user`.id, weight_string(id) from `user` group by id, weight_string(id) order by id asc",
-                "Table": "`user`"
-              },
-              {
-                "OperatorType": "Route",
-                "Variant": "Scatter",
-                "Keyspace": {
-                  "Name": "user",
-                  "Sharded": true
-                },
-                "FieldQuery": "select count(*) from user_extra where 1 != 1",
-                "Query": "select count(*) from user_extra",
-                "Table": "user_extra"
-              }
-            ]
-          }
-        ]
-      }
-    ]
-  }
-}
-
-# group by and ',' joins with condition
-"select user.col from user join user_extra on user_extra.col = user.col group by user.id"
-"unsupported: cross-shard query with aggregates"
-{
-  "QueryType": "SELECT",
-  "Original": "select user.col from user join user_extra on user_extra.col = user.col group by user.id",
-  "Instructions": {
-    "OperatorType": "Aggregate",
-    "Variant": "Ordered",
-    "Aggregates": "random(0) AS col",
-    "GroupBy": "(2|1)",
-    "ResultColumns": 1,
-    "Inputs": [
-      {
-        "OperatorType": "Projection",
-        "Expressions": [
-          "[COLUMN 2] * [COLUMN 3] as col",
-          "[COLUMN 1]",
-          "[COLUMN 0] as id"
-        ],
-        "Inputs": [
-          {
-            "OperatorType": "Join",
-            "Variant": "Join",
-            "JoinColumnIndexes": "L:2,L:3,L:1,R:0",
-            "JoinVars": {
-              "user_col": 0
-            },
-            "TableName": "`user`_user_extra",
-            "Inputs": [
-              {
-                "OperatorType": "Route",
-                "Variant": "Scatter",
-                "Keyspace": {
-                  "Name": "user",
-                  "Sharded": true
-                },
-                "FieldQuery": "select `user`.col, `user`.col, `user`.id, weight_string(`user`.id) from `user` where 1 != 1 group by `user`.col, `user`.id, weight_string(`user`.id)",
-                "OrderBy": "(2|3) ASC",
-                "Query": "select `user`.col, `user`.col, `user`.id, weight_string(`user`.id) from `user` group by `user`.col, `user`.id, weight_string(`user`.id) order by `user`.id asc",
-                "Table": "`user`"
-              },
-              {
-                "OperatorType": "Route",
-                "Variant": "Scatter",
-                "Keyspace": {
-                  "Name": "user",
-                  "Sharded": true
-                },
-                "FieldQuery": "select count(*) from user_extra where 1 != 1",
-                "Query": "select count(*) from user_extra where user_extra.col = :user_col",
-                "Table": "user_extra"
-              }
-            ]
-          }
-        ]
-      }
-    ]
-  }
-}
-
 # subqueries not supported in group by
 "select id from user group by id, (select id from user_extra)"
 "unsupported: subqueries disallowed in GROUP or ORDER BY"
@@ -542,73 +366,6 @@ Gen4 plan same as above
 "Select query does not belong to the same keyspace as the view statement"
 Gen4 plan same as above
 
-# table_schema OR predicate
-# It is unsupported because we do not route queries to multiple keyspaces right now
-"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'ks' OR TABLE_SCHEMA = 'main'"
-{
-  "QueryType": "SELECT",
-  "Original": "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'ks' OR TABLE_SCHEMA = 'main'",
-  "Instructions": {
-    "OperatorType": "Route",
-    "Variant": "DBA",
-    "Keyspace": {
-      "Name": "main",
-      "Sharded": false
-    },
-    "FieldQuery": "select * from INFORMATION_SCHEMA.`TABLES` where 1 != 1",
-    "Query": "select * from INFORMATION_SCHEMA.`TABLES` where TABLE_SCHEMA = 'ks' or TABLE_SCHEMA = 'main'",
-    "Table": "INFORMATION_SCHEMA.`TABLES`"
-  }
-}
-Gen4 plan same as above
-
-# aggr and non-aggr without group by (with query does not give useful result out)
-"select id, count(*) from user"
-{
-  "QueryType": "SELECT",
-  "Original": "select id, count(*) from user",
-  "Instructions": {
-    "OperatorType": "Aggregate",
-    "Variant": "Scalar",
-    "Aggregates": "count(1)",
-    "Inputs": [
-      {
-        "OperatorType": "Route",
-        "Variant": "Scatter",
-        "Keyspace": {
-          "Name": "user",
-          "Sharded": true
-        },
-        "FieldQuery": "select id, count(*) from `user` where 1 != 1",
-        "Query": "select id, count(*) from `user`",
-        "Table": "`user`"
-      }
-    ]
-  }
-}
-{
-  "QueryType": "SELECT",
-  "Original": "select id, count(*) from user",
-  "Instructions": {
-    "OperatorType": "Aggregate",
-    "Variant": "Scalar",
-    "Aggregates": "random(0) AS id, sum(1) AS count(*)",
-    "Inputs": [
-      {
-        "OperatorType": "Route",
-        "Variant": "Scatter",
-        "Keyspace": {
-          "Name": "user",
-          "Sharded": true
-        },
-        "FieldQuery": "select id, count(*) from `user` where 1 != 1",
-        "Query": "select id, count(*) from `user`",
-        "Table": "`user`"
-      }
-    ]
-  }
-}
-
 # avg function on scatter query
 "select avg(id) from user"
 "unsupported: in scatter query: complex aggregate expression"
@@ -619,15 +376,6 @@ Gen4 error: unsupported: in scatter query: aggregation function 'avg'
 "generating order by clause: ambiguous symbol reference: a"
 Gen4 plan same as above
 
-# TODO this should be planned without using OA and MS
-"select u.id from user u join user_extra ue on ue.id = u.id group by u.id having count(u.name) = 3"
-"unsupported: cross-shard query with aggregates"
-Gen4 error: cannot push projections in ordered aggregates
-
-"select (select 1 from user u having count(ue.col) > 10) from user_extra ue"
-"symbol ue.col not found in subquery"
-Gen4 error: cannot push projections in ordered aggregates
-
 # subquery of information_schema with itself and star expression in outer select
 "select a.*, u.id from information_schema.a a, user u where a.id in (select * from information_schema.b)"
 "unsupported: '*' expression in cross-shard query"
diff --git a/web/vtadmin/src/components/routes/keyspace/Advanced.test.tsx b/web/vtadmin/src/components/routes/keyspace/Advanced.test.tsx
new file mode 100644
index 0000000000..1d40c2d511
--- /dev/null
+++ b/web/vtadmin/src/components/routes/keyspace/Advanced.test.tsx
@@ -0,0 +1,109 @@
+/**
+ * Copyright 2022 The Vitess Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { rest } from 'msw';
+import { setupServer } from 'msw/node';
+import { render, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { Advanced } from './Advanced';
+import { vtadmin } from '../../../proto/vtadmin';
+
+const ORIGINAL_PROCESS_ENV = process.env;
+const TEST_PROCESS_ENV = {
+    ...process.env,
+    REACT_APP_VTADMIN_API_ADDRESS: '',
+};
+
+describe('Advanced keyspace actions', () => {
+    const keyspace: vtadmin.IKeyspace = {
+        cluster: { id: 'some-cluster', name: 'some-cluster' },
+        keyspace: {
+            name: 'some-keyspace',
+        },
+    };
+
+    const server = setupServer(
+        rest.get('/api/keyspace/:clusterID/:keyspace', (req, res, ctx) => {
+            return res(ctx.json({ ok: true, result: keyspace }));
+        }),
+        rest.put('/api/schemas/reload', (req, res, ctx) => {
+            return res(ctx.json({ ok: true }));
+        })
+    );
+
+    const queryClient = new QueryClient({
+        defaultOptions: { queries: { retry: false } },
+    });
+
+    beforeAll(() => {
+        process.env = { ...TEST_PROCESS_ENV } as NodeJS.ProcessEnv;
+        server.listen();
+    });
+
+    beforeEach(() => {
+        process.env = { ...TEST_PROCESS_ENV } as NodeJS.ProcessEnv;
+        jest.clearAllMocks();
+    });
+
+    afterAll(() => {
+        process.env = { ...ORIGINAL_PROCESS_ENV };
+        server.close();
+    });
+
+    describe('Reload Schema', () => {
+        it('reloads the schema', async () => {
+            jest.spyOn(global, 'fetch');
+
+            render(
+                <QueryClientProvider client={queryClient}>
+                    <Advanced clusterID="some-cluster" name="some-keyspace" />
+                </QueryClientProvider>
+            );
+
+            expect(screen.getByText('Loading...')).not.toBeNull();
+
+            await waitFor(async () => {
+                expect(screen.queryByText('Loading...')).toBeNull();
+            });
+
+            expect(global.fetch).toHaveBeenCalledTimes(1);
+            expect(global.fetch).toHaveBeenCalledWith(`/api/keyspace/some-cluster/some-keyspace`, {
+                credentials: undefined,
+            });
+
+            jest.clearAllMocks();
+
+            const container = screen.getByTitle('Reload Schema');
+            const button = within(container).getByRole('button');
+            expect(button).not.toHaveAttribute('disabled');
+
+            const user = userEvent.setup();
+            await user.click(button);
+            await waitFor(() => {
+                expect(global.fetch).toHaveBeenCalledTimes(1);
+            });
+
+            expect(global.fetch).toHaveBeenCalledWith(
+                `/api/schemas/reload?cluster=some-cluster&keyspace=some-keyspace`,
+                {
+                    credentials: undefined,
+                    method: 'put',
+                }
+            );
+        });
+    });
+});