Repository: vitessio/vitess
Pull request #11310 (2022-09-21 17:53:09)

Fix conditions after `<=>` operator on left joined table columns being ignored for routing purposes.
## Description

This fixes an edge case where a comparison using the `<=>` operator would prevent the use of any following predicates after for routing purposes (only on left joins).

Here's an example query:

```sql
SELECT music.id FROM music LEFT OUTER JOIN user ON music.user_id = user.id WHERE user.id <=> NULL AND music.user_id = 10
```

This would generate the following plan:

```json
{
  "QueryType": "SELECT",
  "Original": "SELECT music.id FROM music LEFT OUTER JOIN user ON music.user_id = user.id WHERE user.id \u003c=\u003e NULL AND music.user_id = 10",
  "Instructions": {
    "OperatorType": "Route",
    "Variant": "Scatter",
    "Keyspace": {
      "Name": "user",
      "Sharded": true
    },
    "FieldQuery": "select music.id from music left join `user` on music.user_id = `user`.id where 1 != 1",
    "Query": "select music.id from music left join `user` on music.user_id = `user`.id where `user`.id \u003c=\u003e null and music.user_id = 10",
    "Table": "`user`, music"
  },
  "TablesUsed": [
    "user.music",
    "user.user"
  ]
}
```

With this fix in place, this plan is generated instead:

```json
{
  "QueryType": "SELECT",
  "Original": "SELECT music.id FROM music LEFT OUTER JOIN user ON music.user_id = user.id WHERE user.id \u003c=\u003e NULL AND music.user_id = 10",
  "Instructions": {
    "OperatorType": "Route",
    "Variant": "EqualUnique",
    "Keyspace": {
      "Name": "user",
      "Sharded": true
    },
    "FieldQuery": "select music.id from music left join `user` on music.user_id = `user`.id where 1 != 1",
    "Query": "select music.id from music left join `user` on music.user_id = `user`.id where music.user_id = 10 and `user`.id \u003c=\u003e null",
    "Table": "`user`, music",
    "Values": [
      "INT64(10)"
    ],
    "Vindex": "user_index"
  },
  "TablesUsed": [
    "user.music",
    "user.user"
  ]
}
```

## Related Issue(s)

I stumbled upon this issue while working on a fix for https://github.com/vitessio/vitess/issues/11275.

## 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

## Deployment Notes

<!-- Notes regarding deployment of the contained body of work. These should note any db migrations, etc. -->


vitess-bot[bot]: ### Review Checklist
            
Hello reviewers! :wave: Please follow this checklist when reviewing this Pull Request.

#### General
- [ ] Ensure that the Pull Request has a descriptive title.
- [ ] If this is a change that users need to know about, please apply the `release notes (needs details)` label so that merging is blocked unless the summary release notes document is 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. Additionally, flag names should use dashes (`-`) as word separators rather than underscores (`_`).
- [ ] If a workflow is added or modified, each items in `Jobs` should be named in order to mark it as `required`. If the workflow should be required, the GitHub Admin should be notified.

#### 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.

arthurschreiber: The root of this issue is that when pushing predicates to a `Join` operator, we handle nullsafe equality comparisons (and other more complex predicates) by putting them into a `Filter` operator that wraps the `Join`. From that point on, all further predicates get pushed to the `Filter`, thus never reaching the `Join` operator and never being used for routing purposes.

I fixed this by changing the logic for when predicates are pushed into a `Filter` - we first attempt to push the predicate down the operator that is wrapped by the filter. If that returns a new non-`Filter` predicate, we wrap it again into a `Filter`. But if pushing the predicate down returns a `Filter` as well, we merge the filters together.

Patch:
diff --git a/.github/workflows/cluster_endtoend_12.yml b/.github/workflows/cluster_endtoend_12.yml
index be5e30822c..7f794c840c 100644
--- a/.github/workflows/cluster_endtoend_12.yml
+++ b/.github/workflows/cluster_endtoend_12.yml
@@ -6,71 +6,113 @@ concurrency:
   group: format('{0}-{1}', ${{ github.ref }}, 'Cluster (12)')
   cancel-in-progress: true
 
+env:
+  LAUNCHABLE_ORGANIZATION: "vitess"
+  LAUNCHABLE_WORKSPACE: "vitess-app"
+  GITHUB_PR_HEAD_SHA: "${{ github.event.pull_request.head.sha }}"
+
 jobs:
   build:
     name: Run endtoend tests on Cluster (12)
-    runs-on: self-hosted
+    runs-on: ubuntu-18.04
 
     steps:
-      - name: Check if workflow needs to be skipped
-        id: skip-workflow
-        run: |
-          skip='false'
-          if [[ "${{github.event.pull_request}}" ==  "" ]] && [[ "${{github.ref}}" != "refs/heads/main" ]] && [[ ! "${{github.ref}}" =~ ^refs/heads/release-[0-9]+\.[0-9]$ ]] && [[ ! "${{github.ref}}" =~ "refs/tags/.*" ]]; then
-            skip='true'
-          fi
-          echo Skip ${skip}
-          echo "::set-output name=skip-workflow::${skip}"
-
-      - name: Check out code
-        if: steps.skip-workflow.outputs.skip-workflow == 'false'
-        uses: actions/checkout@v2
-
-      - name: Check for changes in relevant files
-        if: steps.skip-workflow.outputs.skip-workflow == 'false'
-        uses: frouioui/paths-filter@main
-        id: changes
-        with:
-          token: ''
-          filters: |
-            end_to_end:
-              - 'go/**/*.go'
-              - 'test.go'
-              - 'Makefile'
-              - 'build.env'
-              - 'go.[sumod]'
-              - 'proto/*.proto'
-              - 'tools/**'
-              - 'config/**'
-              - '.github/docker/**'
-              - 'bootstrap.sh'
-              - '.github/workflows/cluster_endtoend_12.yml'
-
-      - name: Build Docker Image
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: docker build -f ./.github/docker/cluster_test_12/Dockerfile -t cluster_test_12:$GITHUB_SHA  .
-
-      - name: Run test
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        timeout-minutes: 30
-        run: docker run --name "cluster_test_12_$GITHUB_SHA" cluster_test_12:$GITHUB_SHA /bin/bash -c 'source build.env && go run test.go -keep-data=true -docker=false -print-log -follow -shard 12 -- -- --keep-data=true'
-
-      - name: Print Volume Used
-        if: always() && steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker inspect -f '{{ (index .Mounts 0).Name }}' cluster_test_12_$GITHUB_SHA
-
-      - name: Cleanup Docker Volume
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker rm -v cluster_test_12_$GITHUB_SHA
-
-      - name: Cleanup Docker Container
-        if: always() && steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker rm -f cluster_test_12_$GITHUB_SHA
-
-      - name: Cleanup Docker Image
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker image rm cluster_test_12:$GITHUB_SHA
+    - name: Check if workflow needs to be skipped
+      id: skip-workflow
+      run: |
+        skip='false'
+        if [[ "${{github.event.pull_request}}" ==  "" ]] && [[ "${{github.ref}}" != "refs/heads/main" ]] && [[ ! "${{github.ref}}" =~ ^refs/heads/release-[0-9]+\.[0-9]$ ]] && [[ ! "${{github.ref}}" =~ "refs/tags/.*" ]]; then
+          skip='true'
+        fi
+        echo Skip ${skip}
+        echo "::set-output name=skip-workflow::${skip}"
+
+    - name: Check out code
+      if: steps.skip-workflow.outputs.skip-workflow == 'false'
+      uses: actions/checkout@v2
+
+    - name: Check for changes in relevant files
+      if: steps.skip-workflow.outputs.skip-workflow == 'false'
+      uses: frouioui/paths-filter@main
+      id: changes
+      with:
+        token: ''
+        filters: |
+          end_to_end:
+            - 'go/**/*.go'
+            - 'test.go'
+            - 'Makefile'
+            - 'build.env'
+            - 'go.[sumod]'
+            - 'proto/*.proto'
+            - 'tools/**'
+            - 'config/**'
+            - 'bootstrap.sh'
+            - '.github/workflows/cluster_endtoend_12.yml'
+
+    - name: Set up Go
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      uses: actions/setup-go@v2
+      with:
+        go-version: 1.18.5
+
+    - name: Set up python
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      uses: actions/setup-python@v2
+
+    - name: Tune the OS
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+        echo '1024 65535' | sudo tee -a /proc/sys/net/ipv4/ip_local_port_range
+        # Increase the asynchronous non-blocking I/O. More information at https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_use_native_aio
+        echo "fs.aio-max-nr = 1048576" | sudo tee -a /etc/sysctl.conf
+        sudo sysctl -p /etc/sysctl.conf
+
+    - name: Get dependencies
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget eatmydata
+        sudo service mysql stop
+        sudo service etcd stop
+        sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/
+        sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld
+        go mod download
+
+        # install JUnit report formatter
+        go install github.com/vitessio/go-junit-report@HEAD
+
+    - name: Setup launchable dependencies
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+        # Get Launchable CLI installed. If you can, make it a part of the builder image to speed things up
+        pip3 install --user launchable~=1.0 > /dev/null
+
+        # verify that launchable setup is all correct.
+        launchable verify || true
+
+        # Tell Launchable about the build you are producing and testing
+        launchable record build --name "$GITHUB_RUN_ID" --source .
+
+    - name: Run cluster endtoend test
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      timeout-minutes: 30
+      run: |
+        # We set the VTDATAROOT to the /tmp folder to reduce the file path of mysql.sock file
+        # which musn't be more than 107 characters long.
+        export VTDATAROOT="/tmp/"
+        source build.env
+
+        set -x
+
+        # run the tests however you normally do, then produce a JUnit XML file
+        eatmydata -- go run test.go -docker=false -follow -shard 12 | tee -a output.txt | go-junit-report -set-exit-code > report.xml
+
+    - name: Print test output and Record test result in launchable
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true' && always()
+      run: |
+        # send recorded tests to launchable
+        launchable record tests --build "$GITHUB_RUN_ID" go-test . || true
+
+        # print test output
+        cat output.txt
diff --git a/.github/workflows/cluster_endtoend_18.yml b/.github/workflows/cluster_endtoend_18.yml
index 77c2d9b797..08086ca7c0 100644
--- a/.github/workflows/cluster_endtoend_18.yml
+++ b/.github/workflows/cluster_endtoend_18.yml
@@ -6,71 +6,118 @@ concurrency:
   group: format('{0}-{1}', ${{ github.ref }}, 'Cluster (18)')
   cancel-in-progress: true
 
+env:
+  LAUNCHABLE_ORGANIZATION: "vitess"
+  LAUNCHABLE_WORKSPACE: "vitess-app"
+  GITHUB_PR_HEAD_SHA: "${{ github.event.pull_request.head.sha }}"
+
 jobs:
   build:
     name: Run endtoend tests on Cluster (18)
-    runs-on: self-hosted
+    runs-on: ubuntu-18.04
 
     steps:
-      - name: Check if workflow needs to be skipped
-        id: skip-workflow
-        run: |
-          skip='false'
-          if [[ "${{github.event.pull_request}}" ==  "" ]] && [[ "${{github.ref}}" != "refs/heads/main" ]] && [[ ! "${{github.ref}}" =~ ^refs/heads/release-[0-9]+\.[0-9]$ ]] && [[ ! "${{github.ref}}" =~ "refs/tags/.*" ]]; then
-            skip='true'
-          fi
-          echo Skip ${skip}
-          echo "::set-output name=skip-workflow::${skip}"
-
-      - name: Check out code
-        if: steps.skip-workflow.outputs.skip-workflow == 'false'
-        uses: actions/checkout@v2
-
-      - name: Check for changes in relevant files
-        if: steps.skip-workflow.outputs.skip-workflow == 'false'
-        uses: frouioui/paths-filter@main
-        id: changes
-        with:
-          token: ''
-          filters: |
-            end_to_end:
-              - 'go/**/*.go'
-              - 'test.go'
-              - 'Makefile'
-              - 'build.env'
-              - 'go.[sumod]'
-              - 'proto/*.proto'
-              - 'tools/**'
-              - 'config/**'
-              - '.github/docker/**'
-              - 'bootstrap.sh'
-              - '.github/workflows/cluster_endtoend_18.yml'
-
-      - name: Build Docker Image
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: docker build -f ./.github/docker/cluster_test_18/Dockerfile -t cluster_test_18:$GITHUB_SHA  .
-
-      - name: Run test
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        timeout-minutes: 30
-        run: docker run --name "cluster_test_18_$GITHUB_SHA" cluster_test_18:$GITHUB_SHA /bin/bash -c 'source build.env && go run test.go -keep-data=true -docker=false -print-log -follow -shard 18 -- -- --keep-data=true'
-
-      - name: Print Volume Used
-        if: always() && steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker inspect -f '{{ (index .Mounts 0).Name }}' cluster_test_18_$GITHUB_SHA
-
-      - name: Cleanup Docker Volume
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker rm -v cluster_test_18_$GITHUB_SHA
-
-      - name: Cleanup Docker Container
-        if: always() && steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker rm -f cluster_test_18_$GITHUB_SHA
-
-      - name: Cleanup Docker Image
-        if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-        run: |
-          docker image rm cluster_test_18:$GITHUB_SHA
+    - name: Check if workflow needs to be skipped
+      id: skip-workflow
+      run: |
+        skip='false'
+        if [[ "${{github.event.pull_request}}" ==  "" ]] && [[ "${{github.ref}}" != "refs/heads/main" ]] && [[ ! "${{github.ref}}" =~ ^refs/heads/release-[0-9]+\.[0-9]$ ]] && [[ ! "${{github.ref}}" =~ "refs/tags/.*" ]]; then
+          skip='true'
+        fi
+        echo Skip ${skip}
+        echo "::set-output name=skip-workflow::${skip}"
+
+    - name: Check out code
+      if: steps.skip-workflow.outputs.skip-workflow == 'false'
+      uses: actions/checkout@v2
+
+    - name: Check for changes in relevant files
+      if: steps.skip-workflow.outputs.skip-workflow == 'false'
+      uses: frouioui/paths-filter@main
+      id: changes
+      with:
+        token: ''
+        filters: |
+          end_to_end:
+            - 'go/**/*.go'
+            - 'test.go'
+            - 'Makefile'
+            - 'build.env'
+            - 'go.[sumod]'
+            - 'proto/*.proto'
+            - 'tools/**'
+            - 'config/**'
+            - 'bootstrap.sh'
+            - '.github/workflows/cluster_endtoend_18.yml'
+
+    - name: Set up Go
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      uses: actions/setup-go@v2
+      with:
+        go-version: 1.18.5
+
+    - name: Set up python
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      uses: actions/setup-python@v2
+
+    - name: Tune the OS
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+        echo '1024 65535' | sudo tee -a /proc/sys/net/ipv4/ip_local_port_range
+        # Increase the asynchronous non-blocking I/O. More information at https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_use_native_aio
+        echo "fs.aio-max-nr = 1048576" | sudo tee -a /etc/sysctl.conf
+        sudo sysctl -p /etc/sysctl.conf
+
+    - name: Get dependencies
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget eatmydata
+        sudo service mysql stop
+        sudo service etcd stop
+        sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/
+        sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld
+        go mod download
+
+        # install JUnit report formatter
+        go install github.com/vitessio/go-junit-report@HEAD
+
+    - name: Installing zookeeper and consul
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+          make tools
+
+    - name: Setup launchable dependencies
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      run: |
+        # Get Launchable CLI installed. If you can, make it a part of the builder image to speed things up
+        pip3 install --user launchable~=1.0 > /dev/null
+
+        # verify that launchable setup is all correct.
+        launchable verify || true
+
+        # Tell Launchable about the build you are producing and testing
+        launchable record build --name "$GITHUB_RUN_ID" --source .
+
+    - name: Run cluster endtoend test
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
+      timeout-minutes: 30
+      run: |
+        # We set the VTDATAROOT to the /tmp folder to reduce the file path of mysql.sock file
+        # which musn't be more than 107 characters long.
+        export VTDATAROOT="/tmp/"
+        source build.env
+
+        set -x
+
+        # run the tests however you normally do, then produce a JUnit XML file
+        eatmydata -- go run test.go -docker=false -follow -shard 18 | tee -a output.txt | go-junit-report -set-exit-code > report.xml
+
+    - name: Print test output and Record test result in launchable
+      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true' && always()
+      run: |
+        # send recorded tests to launchable
+        launchable record tests --build "$GITHUB_RUN_ID" go-test . || true
+
+        # print test output
+        cat output.txt
diff --git a/Makefile b/Makefile
index 78ab82f1f6..b65e486a90 100644
--- a/Makefile
+++ b/Makefile
@@ -187,11 +187,11 @@ asthelpergen:
 
 sizegen:
 	go run ./go/tools/sizegen/sizegen.go \
-		-in ./go/... \
-	  	-gen vitess.io/vitess/go/pools.Setting \
-	  	-gen vitess.io/vitess/go/vt/vtgate/engine.Plan \
-	  	-gen vitess.io/vitess/go/vt/vttablet/tabletserver.TabletPlan \
-	  	-gen vitess.io/vitess/go/sqltypes.Result
+		--in ./go/... \
+		--gen vitess.io/vitess/go/pools.Setting \
+		--gen vitess.io/vitess/go/vt/vtgate/engine.Plan \
+		--gen vitess.io/vitess/go/vt/vttablet/tabletserver.TabletPlan \
+		--gen vitess.io/vitess/go/sqltypes.Result
 
 astfmtgen:
 	go run ./go/tools/astfmtgen/main.go vitess.io/vitess/go/vt/sqlparser/...
diff --git a/doc/releasenotes/15_0_0_summary.md b/doc/releasenotes/15_0_0_summary.md
index 4eff0ff7be..d52da3adb3 100644
--- a/doc/releasenotes/15_0_0_summary.md
+++ b/doc/releasenotes/15_0_0_summary.md
@@ -299,7 +299,15 @@ After upgrading, the old configurations can be dropped -
 }
 ```
 
-### Default Configuration Files
+#### Default Configuration Files
 
 The default files that VTOrc searches for configurations in have also changed from `"/etc/orchestrator.conf.json", "conf/orchestrator.conf.json", "orchestrator.conf.json"` to
-`"/etc/vtorc.conf.json", "conf/vtorc.conf.json", "vtorc.conf.json"`.
\ No newline at end of file
+`"/etc/vtorc.conf.json", "conf/vtorc.conf.json", "vtorc.conf.json"`.
+
+#### Debug Pages in VTOrc
+
+Like the other vitess binaries (`vtgate`, `vttablet`), now `vtorc` also takes a `--port` flag, on which it 
+displays the `/debug` pages including `/debug/status` and variables it tracks on `/debug/vars`.
+
+This change is backward compatible and opt-in by default. Not specifying the flag works like it used to with
+VTOrc running without displaying these pages.
diff --git a/examples/compose/README.md b/examples/compose/README.md
index 5f216e724b..880c3be9ed 100644
--- a/examples/compose/README.md
+++ b/examples/compose/README.md
@@ -152,6 +152,9 @@ vitess/examples/compose$ ./lvtctl.sh Help
 
 - vtorc web ui:
   http://localhost:13000
+
+- vtorc debug ui:
+  http://localhost:13200
   
 - Stream querylog
   `curl -S localhost:15099/debug/querylog`
diff --git a/examples/compose/docker-compose.beginners.yml b/examples/compose/docker-compose.beginners.yml
index d4402c9167..dd56c7e4de 100644
--- a/examples/compose/docker-compose.beginners.yml
+++ b/examples/compose/docker-compose.beginners.yml
@@ -285,10 +285,12 @@ services:
       - vtctld
       - set_keyspace_durability_policy
     ports:
+      - "13200:8080"
       - "13000:3000"
     volumes:
       - ".:/script"
     environment:
+      - WEB_PORT=8080
       - TOPOLOGY_FLAGS
       - WEB_PORT
       - GRPC_PORT
diff --git a/examples/compose/docker-compose.yml b/examples/compose/docker-compose.yml
index 5dd64b8e0a..faa98c816c 100644
--- a/examples/compose/docker-compose.yml
+++ b/examples/compose/docker-compose.yml
@@ -178,11 +178,13 @@ services:
     environment:
     - TOPOLOGY_FLAGS=--topo_implementation consul --topo_global_server_address consul1:8500
       --topo_global_root vitess/global
+    - WEB_PORT=8080
     - EXTERNAL_DB=0
     - DB_USER=
     - DB_PASS=
     image: vitess/lite:${VITESS_TAG:-latest}
     ports:
+    - 13200:8080
     - 13000:3000
     volumes:
     - .:/script
diff --git a/examples/compose/vtcompose/vtcompose.go b/examples/compose/vtcompose/vtcompose.go
index ca50080e0f..3a5561ff27 100644
--- a/examples/compose/vtcompose/vtcompose.go
+++ b/examples/compose/vtcompose/vtcompose.go
@@ -744,15 +744,17 @@ func generateVTOrc(dbInfo externalDbInfo, keyspaceInfoMap map[string]keyspaceInf
     volumes:
       - ".:/script"
     environment:
-      - TOPOLOGY_FLAGS=%[1]s
-      - EXTERNAL_DB=%[2]s
-      - DB_USER=%[3]s
-      - DB_PASS=%[4]s
+      - WEB_PORT=%[1]d
+      - TOPOLOGY_FLAGS=%[2]s
+      - EXTERNAL_DB=%[3]s
+      - DB_USER=%[4]s
+      - DB_PASS=%[5]s
     ports:
       - "13000:3000"
+      - "13200:%[1]d"
     command: ["sh", "-c", "/script/vtorc-up.sh"]
-    %[5]s
-`, opts.topologyFlags, externalDb, dbInfo.dbUser, dbInfo.dbPass, dependsOn)
+    %[6]s
+`, opts.webPort, opts.topologyFlags, externalDb, dbInfo.dbUser, dbInfo.dbPass, dependsOn)
 }
 
 func generateVreplication(dbInfo externalDbInfo, opts vtOptions) string {
diff --git a/examples/compose/vtorc-up.sh b/examples/compose/vtorc-up.sh
index a1d7492efb..3b66d5eacd 100755
--- a/examples/compose/vtorc-up.sh
+++ b/examples/compose/vtorc-up.sh
@@ -17,6 +17,7 @@
 set -u
 
 external=${EXTERNAL_DB:-0}
+web_port=${WEB_PORT:-'8080'}
 config=${VTORC_CONFIG:-/vt/vtorc/config.json}
 # Copy config directory
 cp -R /script/vtorc /vt
@@ -40,5 +41,6 @@ echo "Starting vtorc..."
 exec /vt/bin/vtorc \
 $TOPOLOGY_FLAGS \
 --logtostderr=true \
+--port $web_port \
 --orc_web_dir=/vt/web/vtorc \
 --config $config
diff --git a/examples/local/scripts/vtorc-up.sh b/examples/local/scripts/vtorc-up.sh
index da5df49b97..88187c03cd 100755
--- a/examples/local/scripts/vtorc-up.sh
+++ b/examples/local/scripts/vtorc-up.sh
@@ -5,6 +5,7 @@ source ./env.sh
 log_dir="${VTDATAROOT}/tmp"
 web_dir="../../web/vtorc"
 vtorc_web_port=16000
+port=16001
 
 vtorc \
   $TOPOLOGY_FLAGS \
@@ -12,6 +13,7 @@ vtorc \
   --logtostderr \
   --alsologtostderr \
   --config="./vtorc/config.json" \
+  --port $port \
   > "${log_dir}/vtorc.out" 2>&1 &
 
 vtorc_pid=$!
@@ -20,6 +22,7 @@ echo ${vtorc_pid} > "${log_dir}/vtorc.pid"
 echo "\
 vtorc is running!
   - UI: http://localhost:${vtorc_web_port}
+  - Debug UI: http://localhost:${port}
   - Logs: ${log_dir}/vtorc.out
   - PID: ${vtorc_pid}
 "
diff --git a/examples/region_sharding/scripts/vtorc-up.sh b/examples/region_sharding/scripts/vtorc-up.sh
index da5df49b97..88187c03cd 100755
--- a/examples/region_sharding/scripts/vtorc-up.sh
+++ b/examples/region_sharding/scripts/vtorc-up.sh
@@ -5,6 +5,7 @@ source ./env.sh
 log_dir="${VTDATAROOT}/tmp"
 web_dir="../../web/vtorc"
 vtorc_web_port=16000
+port=16001
 
 vtorc \
   $TOPOLOGY_FLAGS \
@@ -12,6 +13,7 @@ vtorc \
   --logtostderr \
   --alsologtostderr \
   --config="./vtorc/config.json" \
+  --port $port \
   > "${log_dir}/vtorc.out" 2>&1 &
 
 vtorc_pid=$!
@@ -20,6 +22,7 @@ echo ${vtorc_pid} > "${log_dir}/vtorc.pid"
 echo "\
 vtorc is running!
   - UI: http://localhost:${vtorc_web_port}
+  - Debug UI: http://localhost:${port}
   - Logs: ${log_dir}/vtorc.out
   - PID: ${vtorc_pid}
 "
diff --git a/go/cmd/vtorc/index.go b/go/cmd/vtorc/index.go
new file mode 100644
index 0000000000..dcbe1113e5
--- /dev/null
+++ b/go/cmd/vtorc/index.go
@@ -0,0 +1,28 @@
+/*
+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.
+*/
+
+package main
+
+import (
+	"net/http"
+)
+
+func init() {
+	// Anything unrecognized gets redirected to the status page.
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, "/debug/status", http.StatusFound)
+	})
+}
diff --git a/go/cmd/vtorc/main.go b/go/cmd/vtorc/main.go
index a8a8d33c5c..cc9aee2cc5 100644
--- a/go/cmd/vtorc/main.go
+++ b/go/cmd/vtorc/main.go
@@ -25,7 +25,6 @@ import (
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/spf13/pflag"
 
-	_flag "vitess.io/vitess/go/internal/flag"
 	"vitess.io/vitess/go/vt/grpccommon"
 	"vitess.io/vitess/go/vt/log"
 	vtlog "vitess.io/vitess/go/vt/log"
@@ -81,7 +80,7 @@ func transformArgsForPflag(fs *pflag.FlagSet, args []string) (result []string) {
 			// In the latter case, we don't need to do any transformations, but
 			// in the former, we do.
 			name := strings.SplitN(arg[1:], "=", 2)[0] // discard any potential value (`-myflag` and `-myflag=10` both have the name of `myflag`)
-			if fs.Lookup(name) != nil {
+			if fs.Lookup(name) != nil || name == "help" {
 				// Case 1: We have a long opt with this name, so we need to
 				// prepend an additional hyphen.
 				result = append(result, "-"+arg)
@@ -106,23 +105,12 @@ func main() {
 	grpccommon.RegisterFlags(fs)
 	vtlog.RegisterFlags(fs)
 	logutil.RegisterFlags(fs)
+	servenv.RegisterDefaultFlags()
+	servenv.RegisterFlags()
+	servenv.OnParseFor("vtorc", func(flags *pflag.FlagSet) { flags.AddFlagSet(fs) })
 
 	args := append([]string{}, os.Args...)
 	os.Args = os.Args[0:1]
-	servenv.ParseFlags("vtorc")
-
-	// N.B. This code has to be duplicated from go/internal/flag in order to
-	// correctly transform `-help` => `--help`. Otherwise passing `-help` would
-	// produce:
-	//	unknown shorthand flag: 'e' in -elp
-	var help bool
-	if fs.Lookup("help") == nil {
-		if fs.ShorthandLookup("h") == nil {
-			fs.BoolVarP(&help, "help", "h", false, "display usage and exit")
-		} else {
-			fs.BoolVar(&help, "help", false, "display usage and exit")
-		}
-	}
 
 	configFile := fs.String("config", "", "config file name")
 	sibling := fs.StringP("sibling", "s", "", "sibling instance, host_fqdn[:port]")
@@ -151,12 +139,7 @@ Please update your scripts before the next version, when this will begin to brea
 		log.Warningf(warning, args, os.Args)
 	}
 
-	_flag.Parse(fs)
-	// N.B. Also duplicated from go/internal/flag, for the same reason as above.
-	if help || fs.Arg(0) == "help" {
-		pflag.Usage()
-		os.Exit(0)
-	}
+	servenv.ParseFlags("vtorc")
 
 	if *destination != "" && *sibling != "" {
 		log.Fatalf("-s and -d are synonyms, yet both were specified. You're probably doing the wrong thing.")
@@ -198,5 +181,15 @@ Please update your scripts before the next version, when this will begin to brea
 	config.RuntimeCLIFlags.ConfiguredVersion = AppVersion
 	config.MarkConfigurationLoaded()
 
-	app.HTTP(*discovery)
+	go app.HTTP(*discovery)
+
+	servenv.OnRun(func() {
+		addStatusParts()
+	})
+
+	// For backward compatability, we require that VTOrc functions even when the --port flag is not provided.
+	// In this case, it should function like before but without the servenv pages.
+	// Therefore, currently we don't check for the --port flag to be necessary, but release 16+ that check
+	// can be added to always have the serenv page running in VTOrc.
+	servenv.RunDefault()
 }
diff --git a/go/cmd/vtorc/plugin_prometheusbackend.go b/go/cmd/vtorc/plugin_prometheusbackend.go
new file mode 100644
index 0000000000..868e097ade
--- /dev/null
+++ b/go/cmd/vtorc/plugin_prometheusbackend.go
@@ -0,0 +1,31 @@
+/*
+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.
+*/
+
+package main
+
+// This plugin imports Prometheus to allow for instrumentation
+// with the Prometheus client library
+
+import (
+	"vitess.io/vitess/go/stats/prometheusbackend"
+	"vitess.io/vitess/go/vt/servenv"
+)
+
+func init() {
+	servenv.OnRun(func() {
+		prometheusbackend.Init("vtorc")
+	})
+}
diff --git a/go/cmd/vtorc/status.go b/go/cmd/vtorc/status.go
new file mode 100644
index 0000000000..bdb5496305
--- /dev/null
+++ b/go/cmd/vtorc/status.go
@@ -0,0 +1,30 @@
+/*
+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 agreedto 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.
+*/
+
+package main
+
+import (
+	"vitess.io/vitess/go/vt/servenv"
+	"vitess.io/vitess/go/vt/vtorc/logic"
+)
+
+// addStatusParts adds UI parts to the /debug/status page of VTOrc
+func addStatusParts() {
+	servenv.AddStatusPart("Recent Recoveries", logic.TopologyRecoveriesTemplate, func() any {
+		recoveries, _ := logic.ReadRecentRecoveries("", false, 0)
+		return recoveries
+	})
+}
diff --git a/go/flags/endtoend/vtorc.txt b/go/flags/endtoend/vtorc.txt
new file mode 100644
index 0000000000..49354958c2
--- /dev/null
+++ b/go/flags/endtoend/vtorc.txt
@@ -0,0 +1,82 @@
+Usage of vtorc:
+      --alsologtostderr                          log to standard error as well as files
+      --binlog string                            Binary log file name
+      --catch-sigpipe                            catch and ignore SIGPIPE on stdout and stderr if specified
+      --clusters_to_watch string                 Comma-separated list of keyspaces or keyspace/shards that this instance will monitor and repair. Defaults to all clusters in the topology. Example: "ks1,ks2/-80"
+      --config string                            config file name
+  -d, --destination string                       destination instance, host_fqdn[:port] (synonym to -s)
+      --discovery                                auto discovery mode (default true)
+      --emit_stats                               If set, emit stats to push-based monitoring and stats backends
+      --enable-database-update                   Enable database update, overrides SkipVTOrcDatabaseUpdate
+      --grab-election                            Grab leadership (only applies to continuous mode)
+      --grpc_auth_static_client_creds string     when using grpc_static_auth in the server, this file provides the credentials to use to authenticate with server
+      --grpc_compression string                  Which protocol to use for compressing gRPC. Default: nothing. Supported: snappy
+      --grpc_enable_tracing                      Enable gRPC tracing.
+      --grpc_initial_conn_window_size int        gRPC initial connection window size
+      --grpc_initial_window_size int             gRPC initial window size
+      --grpc_keepalive_time duration             After a duration of this time, if the client doesn't see any activity, it pings the server to see if the transport is still alive. (default 10s)
+      --grpc_keepalive_timeout duration          After having pinged for keepalive check, the client waits for a duration of Timeout and if no activity is seen even after that the connection is closed. (default 10s)
+      --grpc_max_message_size int                Maximum allowed RPC message size. Larger messages will be rejected by gRPC with the error 'exceeding the max size'. (default 16777216)
+      --grpc_prometheus                          Enable gRPC monitoring with Prometheus.
+  -h, --help                                     display usage and exit
+      --ignore-raft-setup                        Override RaftEnabled for CLI invocation (CLI by default not allowed for raft setups). NOTE: operations by CLI invocation may not reflect in all raft nodes.
+      --keep_logs duration                       keep logs for this long (using ctime) (zero to keep forever)
+      --keep_logs_by_mtime duration              keep logs for this long (using mtime) (zero to keep forever)
+      --lameduck-period duration                 keep running at least this long after SIGTERM before stopping (default 50ms)
+      --log_backtrace_at traceLocation           when logging hits line file:N, emit a stack trace (default :0)
+      --log_dir string                           If non-empty, write log files in this directory
+      --log_err_stacks                           log stack traces for errors
+      --log_rotate_max_size uint                 size in bytes at which logs are rotated (glog.MaxSize) (default 1887436800)
+      --logtostderr                              log to standard error instead of files
+      --mysql_server_flush_delay duration        Delay after which buffered response will be flushed to the client. (default 100ms)
+      --noop                                     Dry run; do not perform destructing operations
+      --onclose_timeout duration                 wait no more than this for OnClose handlers before stopping (default 1ns)
+      --onterm_timeout duration                  wait no more than this for OnTermSync handlers before stopping (default 10s)
+      --orc_web_dir string                       VTOrc http file location (default "web/vtorc")
+      --pid_file string                          If set, the process will write its pid to the named file, and delete it on graceful shutdown.
+      --port int                                 port for the server
+      --pprof strings                            enable profiling
+      --promotion-rule string                    Promotion rule for register-andidate (prefer|neutral|prefer_not|must_not) (default "prefer")
+      --purge_logs_interval duration             how often try to remove old logs (default 1h0m0s)
+      --remote_operation_timeout duration        time to wait for a remote operation (default 30s)
+      --security_policy string                   the name of a registered security policy to use for controlling access to URLs - empty means allow all for anyone (built-in policies: deny-all, read-only)
+      --shutdown_wait_time duration              maximum time to wait for vtorc to release all the locks that it is holding before shutting down on SIGTERM (default 30s)
+  -s, --sibling string                           sibling instance, host_fqdn[:port]
+      --skip-continuous-registration             Skip cli commands performaing continuous registration (to reduce orchestratrator backend db load
+      --skip-unresolve                           Do not unresolve a host name
+      --skip-unresolve-check                     Skip/ignore checking an unresolve mapping (via hostname_unresolve table) resolves back to same hostname
+      --statement string                         Statement/hint
+      --stats_backend string                     The name of the registered push-based monitoring/stats backend to use
+      --stats_combine_dimensions string          List of dimensions to be combined into a single "all" value in exported stats vars
+      --stats_common_tags string                 Comma-separated list of common tags for the stats backend. It provides both label and values. Example: label1:value1,label2:value2
+      --stats_drop_variables string              Variables to be dropped from the list of exported variables.
+      --stats_emit_period duration               Interval between emitting stats to all registered backends (default 1m0s)
+      --stderrthreshold severity                 logs at or above this threshold go to stderr (default 1)
+      --tablet_manager_grpc_ca string            the server ca to use to validate servers when connecting
+      --tablet_manager_grpc_cert string          the cert to use to connect
+      --tablet_manager_grpc_concurrency int      concurrency to use to talk to a vttablet server for performance-sensitive RPCs (like ExecuteFetchAs{Dba,AllPrivs,App}) (default 8)
+      --tablet_manager_grpc_connpool_size int    number of tablets to keep tmclient connections open to (default 100)
+      --tablet_manager_grpc_crl string           the server crl to use to validate server certificates when connecting
+      --tablet_manager_grpc_key string           the key to use to connect
+      --tablet_manager_grpc_server_name string   the server name to use to validate server certificate
+      --tablet_manager_protocol string           Protocol to use to make tabletmanager RPCs to vttablets. (default "grpc")
+      --tag string                               tag to add ('tagname' or 'tagname=tagvalue') or to search ('tagname' or 'tagname=tagvalue' or comma separated 'tag0,tag1=val1,tag2' for intersection of all)
+      --topo_etcd_lease_ttl int                  Lease TTL for locks and leader election. The client will use KeepAlive to keep the lease going. (default 30)
+      --topo_etcd_tls_ca string                  path to the ca to use to validate the server cert when connecting to the etcd topo server
+      --topo_etcd_tls_cert string                path to the client cert to use to connect to the etcd topo server, requires topo_etcd_tls_key, enables TLS
+      --topo_etcd_tls_key string                 path to the client key to use to connect to the etcd topo server, enables TLS
+      --topo_global_root string                  the path of the global topology data in the global topology server
+      --topo_global_server_address string        the address of the global topology server
+      --topo_implementation string               the topology implementation to use
+      --topo_k8s_context string                  The kubeconfig context to use, overrides the 'current-context' from the config
+      --topo_k8s_kubeconfig string               Path to a valid kubeconfig file. When running as a k8s pod inside the same cluster you wish to use as the topo, you may omit this and the below arguments, and Vitess is capable of auto-discovering the correct values. https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod
+      --topo_k8s_namespace string                The kubernetes namespace to use for all objects. Default comes from the context or in-cluster config
+      --topo_zk_auth_file string                 auth to use when connecting to the zk topo server, file contents should be <scheme>:<auth>, e.g., digest:user:pass
+      --topo_zk_base_timeout duration            zk base timeout (see zk.Connect) (default 30s)
+      --topo_zk_max_concurrency int              maximum number of pending requests to send to a Zookeeper server. (default 64)
+      --topo_zk_tls_ca string                    the server ca to use to validate servers when connecting to the zk topo server
+      --topo_zk_tls_cert string                  the cert to use to connect to the zk topo server, requires topo_zk_tls_key, enables TLS
+      --topo_zk_tls_key string                   the key to use to connect to the zk topo server, enables TLS
+  -v, --v Level                                  log level for V logs
+      --version                                  print binary version
+      --vmodule moduleSpec                       comma-separated list of pattern=N settings for file-filtered logging
diff --git a/go/tools/sizegen/sizegen.go b/go/tools/sizegen/sizegen.go
index 41519daee8..63ef88dd07 100644
--- a/go/tools/sizegen/sizegen.go
+++ b/go/tools/sizegen/sizegen.go
@@ -18,7 +18,6 @@ package main
 
 import (
 	"bytes"
-	"flag"
 	"fmt"
 	"go/types"
 	"log"
@@ -27,12 +26,13 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/dave/jennifer/jen"
+	"github.com/spf13/pflag"
+	"golang.org/x/tools/go/packages"
+
 	"vitess.io/vitess/go/hack"
 	"vitess.io/vitess/go/tools/common"
 	"vitess.io/vitess/go/tools/goimports"
-
-	"github.com/dave/jennifer/jen"
-	"golang.org/x/tools/go/packages"
 )
 
 const licenseFileHeader = `Copyright 2021 The Vitess Authors.
@@ -495,14 +495,15 @@ func (t *typePaths) Set(path string) error {
 }
 
 func main() {
-	var patterns typePaths
-	var generate typePaths
-	var verify bool
-
-	flag.Var(&patterns, "in", "Go packages to load the generator")
-	flag.Var(&generate, "gen", "Typename of the Go struct to generate size info for")
-	flag.BoolVar(&verify, "verify", false, "ensure that the generated files are correct")
-	flag.Parse()
+	var (
+		patterns, generate []string
+		verify             bool
+	)
+
+	pflag.StringSliceVar(&patterns, "in", nil, "Go packages to load the generator")
+	pflag.StringSliceVar(&generate, "gen", nil, "Typename of the Go struct to generate size info for")
+	pflag.BoolVar(&verify, "verify", false, "ensure that the generated files are correct")
+	pflag.Parse()
 
 	result, err := GenerateSizeHelpers(patterns, generate)
 	if err != nil {
diff --git a/go/vt/vtctl/grpcvtctldclient/client.go b/go/vt/vtctl/grpcvtctldclient/client.go
index 2dc57a546f..497867aebb 100644
--- a/go/vt/vtctl/grpcvtctldclient/client.go
+++ b/go/vt/vtctl/grpcvtctldclient/client.go
@@ -46,7 +46,7 @@ type gRPCVtctldClient struct {
 }
 
 //go:generate -command grpcvtctldclient go run ../vtctldclient/codegen
-//go:generate grpcvtctldclient -out client_gen.go
+//go:generate grpcvtctldclient --out client_gen.go
 
 func gRPCVtctldClientFactory(addr string) (vtctldclient.VtctldClient, error) {
 	opt, err := grpcclientcommon.SecureDialOption()
diff --git a/go/vt/vtctl/localvtctldclient/client.go b/go/vt/vtctl/localvtctldclient/client.go
index b870247fe0..f94f112403 100644
--- a/go/vt/vtctl/localvtctldclient/client.go
+++ b/go/vt/vtctl/localvtctldclient/client.go
@@ -38,7 +38,7 @@ type localVtctldClient struct {
 func (client *localVtctldClient) Close() error { return nil }
 
 //go:generate -command localvtctldclient go run ../vtctldclient/codegen
-//go:generate localvtctldclient -targetpkg localvtctldclient -impl localVtctldClient -out client_gen.go -local
+//go:generate localvtctldclient --targetpkg localvtctldclient --impl localVtctldClient --out client_gen.go --local
 
 // New returns a local vtctldclient.VtctldClient that makes method calls on the
 // provided VtctldServer implementation. No network traffic takes place between
diff --git a/go/vt/vtctl/reparentutil/planned_reparenter.go b/go/vt/vtctl/reparentutil/planned_reparenter.go
index 4f46d234e2..f95329aa25 100644
--- a/go/vt/vtctl/reparentutil/planned_reparenter.go
+++ b/go/vt/vtctl/reparentutil/planned_reparenter.go
@@ -184,6 +184,21 @@ func (pr *PlannedReparenter) preflightChecks(
 		return true, vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "primary-elect tablet %v is not in the shard", primaryElectAliasStr)
 	}
 
+	// PRS is only meant to be called when all the tablets are healthy.
+	// So we assume that all the tablets are reachable and check if the primary elect will be able
+	// to make progress if it is promoted. This is needed because sometimes users may ask to promote
+	// a tablet which can never make progress. For example, let's say the user has a durability policy
+	// where they require 2 semi-sync acks but from cross-cell replicas.
+	// Let's say they have 3 replicas A in zone 1 and B and C in zone 2. In this case, A is the only
+	// eligible primary elect. Both B and C won't be able to make forward progress if they are promoted.
+	var tabletsReachable []*topodatapb.Tablet
+	for _, info := range tabletMap {
+		tabletsReachable = append(tabletsReachable, info.Tablet)
+	}
+	if !canEstablishForTablet(opts.durability, newPrimaryTabletInfo.Tablet, tabletsReachable) {
+		return true, vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "primary-elect tablet %v won't be able to make forward progress on promotion", primaryElectAliasStr)
+	}
+
 	ev.NewPrimary = proto.Clone(newPrimaryTabletInfo.Tablet).(*topodatapb.Tablet)
 
 	return false, nil
diff --git a/go/vt/vtctl/vtctldclient/codegen/main.go b/go/vt/vtctl/vtctldclient/codegen/main.go
index 0aec3ad520..baeb7af8d0 100644
--- a/go/vt/vtctl/vtctldclient/codegen/main.go
+++ b/go/vt/vtctl/vtctldclient/codegen/main.go
@@ -18,7 +18,6 @@ package main
 
 import (
 	"errors"
-	"flag"
 	"fmt"
 	"go/types"
 	"io"
@@ -27,33 +26,34 @@ import (
 	"regexp"
 	"sort"
 
+	"github.com/spf13/pflag"
 	"golang.org/x/tools/go/packages"
 )
 
 func main() { // nolint:funlen
-	source := flag.String("source", "../../proto/vtctlservice", "source package")
-	typeName := flag.String("type", "VtctldClient", "interface type to implement")
-	implType := flag.String("impl", "gRPCVtctldClient", "type implementing the interface")
-	pkgName := flag.String("targetpkg", "grpcvtctldclient", "package name to generate code for")
-	local := flag.Bool("local", false, "generate a local, in-process client rather than a grpcclient")
-	out := flag.String("out", "", "output destination. leave empty to use stdout")
+	source := pflag.String("source", "../../proto/vtctlservice", "source package")
+	typeName := pflag.String("type", "VtctldClient", "interface type to implement")
+	implType := pflag.String("impl", "gRPCVtctldClient", "type implementing the interface")
+	pkgName := pflag.String("targetpkg", "grpcvtctldclient", "package name to generate code for")
+	local := pflag.Bool("local", false, "generate a local, in-process client rather than a grpcclient")
+	out := pflag.String("out", "", "output destination. leave empty to use stdout")
 
-	flag.Parse()
+	pflag.Parse()
 
 	if *source == "" {
-		panic("-source cannot be empty")
+		panic("--source cannot be empty")
 	}
 
 	if *typeName == "" {
-		panic("-type cannot be empty")
+		panic("--type cannot be empty")
 	}
 
 	if *implType == "" {
-		panic("-impl cannot be empty")
+		panic("--impl cannot be empty")
 	}
 
 	if *pkgName == "" {
-		panic("-targetpkg cannot be empty")
+		panic("--targetpkg cannot be empty")
 	}
 
 	var output io.Writer = os.Stdout
diff --git a/go/vt/vtgate/planbuilder/abstract/filter.go b/go/vt/vtgate/planbuilder/abstract/filter.go
index 0408f46bc1..46b564e7b9 100644
--- a/go/vt/vtgate/planbuilder/abstract/filter.go
+++ b/go/vt/vtgate/planbuilder/abstract/filter.go
@@ -48,8 +48,20 @@ func (f *Filter) CheckValid() error {
 
 // PushPredicate implements the LogicalOperator interface
 func (f *Filter) PushPredicate(expr sqlparser.Expr, semTable *semantics.SemTable) (LogicalOperator, error) {
-	f.Predicates = append(f.Predicates, expr)
-	return f, nil
+	op, err := f.Source.PushPredicate(expr, semTable)
+	if err != nil {
+		return nil, err
+	}
+
+	if filter, isFilter := op.(*Filter); isFilter {
+		filter.Predicates = append(f.Predicates, filter.Predicates...)
+		return filter, err
+	}
+
+	return &Filter{
+		Source:     op,
+		Predicates: f.Predicates,
+	}, nil
 }
 
 // Compact implements the LogicalOperator interface
diff --git a/go/vt/vtgate/semantics/binder.go b/go/vt/vtgate/semantics/binder.go
index 5045a7a310..543aa53539 100644
--- a/go/vt/vtgate/semantics/binder.go
+++ b/go/vt/vtgate/semantics/binder.go
@@ -114,18 +114,32 @@ func (b *binder) up(cursor *sqlparser.Cursor) error {
 			b.typer.setTypeFor(node, *deps.typ)
 		}
 	case *sqlparser.CountStar:
-		scope := b.scoper.currentScope()
-		var ts TableSet
-		for _, table := range scope.tables {
-			expr := table.getExpr()
+		b.bindCountStar(node)
+	}
+	return nil
+}
+
+func (b *binder) bindCountStar(node *sqlparser.CountStar) {
+	scope := b.scoper.currentScope()
+	var ts TableSet
+	for _, tbl := range scope.tables {
+		switch tbl := tbl.(type) {
+		case *vTableInfo:
+			for _, col := range tbl.cols {
+				if sqlparser.EqualsExpr(node, col) {
+					ts.MergeInPlace(b.recursive[col])
+				}
+			}
+		default:
+			expr := tbl.getExpr()
 			if expr != nil {
-				ts.MergeInPlace(b.tc.tableSetFor(expr))
+				setFor := b.tc.tableSetFor(expr)
+				ts.MergeInPlace(setFor)
 			}
 		}
-		b.recursive[node] = ts
-		b.direct[node] = ts
 	}
-	return nil
+	b.recursive[node] = ts
+	b.direct[node] = ts
 }
 
 func (b *binder) rewriteJoinUsingColName(deps dependency, node *sqlparser.ColName, currentScope *scope) (dependency, error) {
diff --git a/go/vt/vtgate/semantics/early_rewriter.go b/go/vt/vtgate/semantics/early_rewriter.go
index e57828f5bb..6f006efd41 100644
--- a/go/vt/vtgate/semantics/early_rewriter.go
+++ b/go/vt/vtgate/semantics/early_rewriter.go
@@ -33,33 +33,19 @@ type earlyRewriter struct {
 
 func (r *earlyRewriter) down(cursor *sqlparser.Cursor) error {
 	switch node := cursor.Node().(type) {
+	case *sqlparser.Where:
+		if node.Type != sqlparser.HavingClause {
+			return nil
+		}
+		rewriteHavingAndOrderBy(cursor, node)
 	case sqlparser.SelectExprs:
 		_, isSel := cursor.Parent().(*sqlparser.Select)
 		if !isSel {
 			return nil
 		}
-		currentScope := r.scoper.currentScope()
-		var selExprs sqlparser.SelectExprs
-		changed := false
-		for _, selectExpr := range node {
-			starExpr, isStarExpr := selectExpr.(*sqlparser.StarExpr)
-			if !isStarExpr {
-				selExprs = append(selExprs, selectExpr)
-				continue
-			}
-			starExpanded, colNames, err := expandTableColumns(starExpr, currentScope.tables, r.binder.usingJoinInfo, r.scoper.org)
-			if err != nil {
-				return err
-			}
-			if !starExpanded || colNames == nil {
-				selExprs = append(selExprs, selectExpr)
-				continue
-			}
-			selExprs = append(selExprs, colNames...)
-			changed = true
-		}
-		if changed {
-			cursor.ReplaceAndRevisit(selExprs)
+		err := r.expandStar(cursor, node)
+		if err != nil {
+			return err
 		}
 	case *sqlparser.JoinTableExpr:
 		if node.Join == sqlparser.StraightJoinType {
@@ -68,6 +54,7 @@ func (r *earlyRewriter) down(cursor *sqlparser.Cursor) error {
 		}
 	case *sqlparser.Order:
 		r.clause = "order clause"
+		rewriteHavingAndOrderBy(cursor, node)
 	case sqlparser.GroupBy:
 		r.clause = "group statement"
 
@@ -95,6 +82,60 @@ func (r *earlyRewriter) down(cursor *sqlparser.Cursor) error {
 	return nil
 }
 
+func (r *earlyRewriter) expandStar(cursor *sqlparser.Cursor, node sqlparser.SelectExprs) error {
+	currentScope := r.scoper.currentScope()
+	var selExprs sqlparser.SelectExprs
+	changed := false
+	for _, selectExpr := range node {
+		starExpr, isStarExpr := selectExpr.(*sqlparser.StarExpr)
+		if !isStarExpr {
+			selExprs = append(selExprs, selectExpr)
+			continue
+		}
+		starExpanded, colNames, err := expandTableColumns(starExpr, currentScope.tables, r.binder.usingJoinInfo, r.scoper.org)
+		if err != nil {
+			return err
+		}
+		if !starExpanded || colNames == nil {
+			selExprs = append(selExprs, selectExpr)
+			continue
+		}
+		selExprs = append(selExprs, colNames...)
+		changed = true
+	}
+	if changed {
+		cursor.ReplaceAndRevisit(selExprs)
+	}
+	return nil
+}
+
+func rewriteHavingAndOrderBy(cursor *sqlparser.Cursor, node sqlparser.SQLNode) {
+	sel, isSel := cursor.Parent().(*sqlparser.Select)
+	if !isSel {
+		return
+	}
+	sqlparser.Rewrite(node, func(inner *sqlparser.Cursor) bool {
+		switch col := inner.Node().(type) {
+		case *sqlparser.Subquery:
+			return false
+		case *sqlparser.ColName:
+			if !col.Qualifier.IsEmpty() {
+				return false
+			}
+			for _, e := range sel.SelectExprs {
+				ae, ok := e.(*sqlparser.AliasedExpr)
+				if !ok {
+					continue
+				}
+				if ae.As.Equal(col.Name) {
+					inner.Replace(ae.Expr)
+				}
+			}
+		}
+		return true
+	}, nil)
+}
+
 func (r *earlyRewriter) rewriteOrderByExpr(node *sqlparser.Literal) (sqlparser.Expr, error) {
 	currScope, found := r.scoper.specialExprScopes[node]
 	if !found {
diff --git a/go/vt/vtorc/app/http.go b/go/vt/vtorc/app/http.go
index de689d1ca6..3b4e848471 100644
--- a/go/vt/vtorc/app/http.go
+++ b/go/vt/vtorc/app/http.go
@@ -171,5 +171,5 @@ func standardHTTP(continuousDiscovery bool) {
 			log.Fatal(err)
 		}
 	}
-	log.Info("Web server started")
+	log.Info("Web server closed")
 }
diff --git a/go/vt/vtorc/logic/topology_recovery_status.go b/go/vt/vtorc/logic/topology_recovery_status.go
new file mode 100644
index 0000000000..4e85b0529d
--- /dev/null
+++ b/go/vt/vtorc/logic/topology_recovery_status.go
@@ -0,0 +1,48 @@
+/*
+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 agreedto 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.
+*/
+
+package logic
+
+// TopologyRecoveriesTemplate is the HTML to use to display the
+// topology recovery steps list object
+const TopologyRecoveriesTemplate = `
+<style>
+  table {
+    border-collapse: collapse;
+  }
+  td, th {
+    border: 1px solid #999;
+    padding: 0.2rem;
+  }
+</style>
+<table>
+  <tr>
+    <th colspan="4">Recent Recoveries Performed</th>
+  </tr>
+  <tr>
+    <th>Recovery ID</th>
+    <th>Failure Type</th>
+    <th>Instance</th>
+  </tr>
+  {{range $i, $recovery := .}}
+  <tr>
+    <td>{{$recovery.ID}}</td>
+    <td>{{$recovery.AnalysisEntry.Analysis}}</td>
+    <td>{{$recovery.AnalysisEntry.AnalyzedInstanceKey}}</td>
+  </tr>
+  {{end}}
+</table>
+`


Test patch:
diff --git a/.github/docker/cluster_test_12/Dockerfile b/.github/docker/cluster_test_12/Dockerfile
deleted file mode 100644
index 0497e4112d..0000000000
--- a/.github/docker/cluster_test_12/Dockerfile
+++ /dev/null
@@ -1,34 +0,0 @@
-# DO NOT MODIFY: THIS FILE IS GENERATED USING "make generate_ci_workflows"
-
-ARG bootstrap_version=11
-ARG image="vitess/bootstrap:${bootstrap_version}-mysql57"
-
-FROM "${image}"
-
-USER root
-
-# Re-copy sources from working tree
-RUN rm -rf /vt/src/vitess.io/vitess/*
-COPY . /vt/src/vitess.io/vitess
-
-# Set the working directory
-WORKDIR /vt/src/vitess.io/vitess
-
-# Fix permissions
-RUN chown -R vitess:vitess /vt
-
-USER vitess
-
-# Set environment variables
-ENV VTROOT /vt/src/vitess.io/vitess
-# Set the vtdataroot such that it uses the volume mount
-ENV VTDATAROOT /vt/vtdataroot
-
-# create the vtdataroot directory
-RUN mkdir -p $VTDATAROOT
-
-# install goimports
-RUN go install golang.org/x/tools/cmd/goimports@latest
-
-# sleep for 50 minutes
-CMD sleep 3000
diff --git a/.github/docker/cluster_test_18/Dockerfile b/.github/docker/cluster_test_18/Dockerfile
deleted file mode 100644
index 024a441452..0000000000
--- a/.github/docker/cluster_test_18/Dockerfile
+++ /dev/null
@@ -1,37 +0,0 @@
-# DO NOT MODIFY: THIS FILE IS GENERATED USING "make generate_ci_workflows"
-
-ARG bootstrap_version=11
-ARG image="vitess/bootstrap:${bootstrap_version}-mysql57"
-
-FROM "${image}"
-
-USER root
-
-# Re-copy sources from working tree
-RUN rm -rf /vt/src/vitess.io/vitess/*
-COPY . /vt/src/vitess.io/vitess
-
-# Set the working directory
-WORKDIR /vt/src/vitess.io/vitess
-
-# Fix permissions
-RUN chown -R vitess:vitess /vt
-
-USER vitess
-
-# Set environment variables
-ENV VTROOT /vt/src/vitess.io/vitess
-# Set the vtdataroot such that it uses the volume mount
-ENV VTDATAROOT /vt/vtdataroot
-
-# create the vtdataroot directory
-RUN mkdir -p $VTDATAROOT
-
-# install goimports
-RUN go install golang.org/x/tools/cmd/goimports@latest
-
-# make tools
-RUN make tools
-
-# sleep for 50 minutes
-CMD sleep 3000
diff --git a/examples/compose/vtcompose/docker-compose.test.yml b/examples/compose/vtcompose/docker-compose.test.yml
index 3bab7815e2..f05f032a44 100644
--- a/examples/compose/vtcompose/docker-compose.test.yml
+++ b/examples/compose/vtcompose/docker-compose.test.yml
@@ -193,6 +193,7 @@ services:
       - set_keyspace_durability_policy_test_keyspace
       - vtctld
     environment:
+      - WEB_PORT=8080
       - TOPOLOGY_FLAGS=--topo_implementation consul --topo_global_server_address consul1:8500
         --topo_global_root vitess/global
       - EXTERNAL_DB=0
@@ -201,6 +202,7 @@ services:
     image: vitess/lite:${VITESS_TAG:-latest}
     ports:
       - 13000:3000
+      - 13200:8080
     volumes:
       - .:/script
   vttablet101:
diff --git a/go/flags/endtoend/flags_test.go b/go/flags/endtoend/flags_test.go
index cca23c9003..be3db2a8cf 100644
--- a/go/flags/endtoend/flags_test.go
+++ b/go/flags/endtoend/flags_test.go
@@ -47,12 +47,16 @@ var (
 	//go:embed vtctld.txt
 	vtctldTxt string
 
+	//go:embed vtorc.txt
+	vtorcTxt string
+
 	helpOutput = map[string]string{
 		"vtexplain": vtexplainTxt,
 		"vtgate":    vtgateTxt,
 		"vtgr":      vtgrTxt,
 		"vttablet":  vttabletTxt,
 		"vtctld":    vtctldTxt,
+		"vtorc":     vtorcTxt,
 	}
 )
 
diff --git a/go/test/endtoend/cluster/vtorc_process.go b/go/test/endtoend/cluster/vtorc_process.go
index c587632d4c..d8563243a6 100644
--- a/go/test/endtoend/cluster/vtorc_process.go
+++ b/go/test/endtoend/cluster/vtorc_process.go
@@ -98,8 +98,8 @@ func (orc *VTOrcProcess) Setup() (err error) {
 	}
 
 	/* minimal command line arguments:
-	$ vtorc -topo_implementation etcd2 -topo_global_server_address localhost:2379 -topo_global_root /vitess/global
-	-config config/vtorc/default.json -alsologtostderr http
+	$ vtorc --topo_implementation etcd2 --topo_global_server_address localhost:2379 --topo_global_root /vitess/global
+	--config config/vtorc/default.json --alsologtostderr
 	*/
 	orc.proc = exec.Command(
 		orc.Binary,
@@ -114,14 +114,14 @@ func (orc *VTOrcProcess) Setup() (err error) {
 	}
 
 	orc.proc.Args = append(orc.proc.Args, orc.ExtraArgs...)
-	orc.proc.Args = append(orc.proc.Args, "--alsologtostderr", "http")
+	orc.proc.Args = append(orc.proc.Args, "--alsologtostderr")
 
 	errFile, _ := os.Create(path.Join(orc.LogDir, fmt.Sprintf("orc-stderr-%d.txt", timeNow)))
 	orc.proc.Stderr = errFile
 
 	orc.proc.Env = append(orc.proc.Env, os.Environ()...)
 
-	log.Infof("Running orc with command: %v", strings.Join(orc.proc.Args, " "))
+	log.Infof("Running vtorc with command: %v", strings.Join(orc.proc.Args, " "))
 
 	err = orc.proc.Start()
 	if err != nil {
diff --git a/go/vt/vtctl/reparentutil/planned_reparenter_flaky_test.go b/go/vt/vtctl/reparentutil/planned_reparenter_flaky_test.go
index 09900c4a62..5c79caeadb 100644
--- a/go/vt/vtctl/reparentutil/planned_reparenter_flaky_test.go
+++ b/go/vt/vtctl/reparentutil/planned_reparenter_flaky_test.go
@@ -635,6 +635,7 @@ func TestPlannedReparenter_preflightChecks(t *testing.T) {
 					Cell: "zone1",
 					Uid:  500,
 				},
+				durability: &durabilityNone{},
 			},
 			expectedIsNoop: false,
 			expectedEvent: &events.Reparent{
@@ -662,6 +663,7 @@ func TestPlannedReparenter_preflightChecks(t *testing.T) {
 					Cell: "zone1",
 					Uid:  100,
 				},
+				durability: &durabilityNone{},
 			},
 			shouldErr: false,
 		},
@@ -827,6 +829,81 @@ func TestPlannedReparenter_preflightChecks(t *testing.T) {
 			},
 			shouldErr: false,
 		},
+		{
+			name: "primary elect can't make forward progress",
+			tmc: &testutil.TabletManagerClient{
+				ReplicationStatusResults: map[string]struct {
+					Position *replicationdatapb.Status
+					Error    error
+				}{
+					"zone1-0000000100": { // most advanced position
+						Position: &replicationdatapb.Status{
+							Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-10",
+						},
+					},
+					"zone1-0000000101": {
+						Position: &replicationdatapb.Status{
+							Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
+						},
+					},
+				},
+			},
+			ev: &events.Reparent{
+				ShardInfo: *topo.NewShardInfo("testkeyspace", "-", &topodatapb.Shard{
+					PrimaryAlias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  500,
+					},
+				}, nil),
+			},
+			tabletMap: map[string]*topo.TabletInfo{
+				"zone1-0000000100": {
+					Tablet: &topodatapb.Tablet{
+						Alias: &topodatapb.TabletAlias{
+							Cell: "zone1",
+							Uid:  100,
+						},
+						Type: topodatapb.TabletType_REPLICA,
+					},
+				},
+				"zone1-0000000101": {
+					Tablet: &topodatapb.Tablet{
+						Alias: &topodatapb.TabletAlias{
+							Cell: "zone1",
+							Uid:  101,
+						},
+						Type: topodatapb.TabletType_REPLICA,
+					},
+				},
+				"zone1-0000000500": {
+					Tablet: &topodatapb.Tablet{
+						Alias: &topodatapb.TabletAlias{
+							Cell: "zone1",
+							Uid:  500,
+						},
+						Type: topodatapb.TabletType_PRIMARY,
+					},
+				},
+			},
+			opts: &PlannedReparentOptions{
+				// Avoid the current primary.
+				AvoidPrimaryAlias: &topodatapb.TabletAlias{
+					Cell: "zone1",
+					Uid:  500,
+				},
+				durability: &durabilityCrossCell{},
+			},
+			expectedIsNoop: true,
+			expectedEvent: &events.Reparent{
+				ShardInfo: *topo.NewShardInfo("testkeyspace", "-", &topodatapb.Shard{
+					PrimaryAlias: &topodatapb.TabletAlias{
+						Cell: "zone1",
+						Uid:  500,
+					},
+				}, nil),
+			},
+			shouldErr: true,
+		},
 	}
 
 	ctx := context.Background()
@@ -849,6 +926,11 @@ func TestPlannedReparenter_preflightChecks(t *testing.T) {
 			}()
 
 			pr := NewPlannedReparenter(tt.ts, tt.tmc, logger)
+			if tt.opts.durability == nil {
+				durability, err := GetDurabilityPolicy("none")
+				require.NoError(t, err)
+				tt.opts.durability = durability
+			}
 			isNoop, err := pr.preflightChecks(ctx, tt.ev, tt.keyspace, tt.shard, tt.tabletMap, tt.opts)
 			if tt.shouldErr {
 				assert.Error(t, err)
diff --git a/go/vt/vtgate/planbuilder/rewrite_test.go b/go/vt/vtgate/planbuilder/rewrite_test.go
index d4815deebb..b2e9fc7683 100644
--- a/go/vt/vtgate/planbuilder/rewrite_test.go
+++ b/go/vt/vtgate/planbuilder/rewrite_test.go
@@ -120,10 +120,10 @@ func TestHavingRewrite(t *testing.T) {
 		output: "select 1 from t1 where (x = 1 or y = 2) and a = 1 having count(*) = 1",
 	}, {
 		input:  "select count(*) k from t1 where x = 1 or y = 2 having a = 1 and k = 1",
-		output: "select count(*) as k from t1 where (x = 1 or y = 2) and a = 1 having k = 1",
+		output: "select count(*) as k from t1 where (x = 1 or y = 2) and a = 1 having count(*) = 1",
 	}, {
 		input:  "select count(*) k from t1 having k = 10",
-		output: "select count(*) as k from t1 having k = 10",
+		output: "select count(*) as k from t1 having count(*) = 10",
 	}, {
 		input:  "select 1 from t1 where x in (select 1 from t2 having a = 1)",
 		output: "select 1 from t1 where :__sq_has_values1 = 1 and x in ::__sq1",
diff --git a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
index 49042c1056..15d7a04d50 100644
--- a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
@@ -1073,7 +1073,7 @@ Gen4 error: unsupported: '*' expression in cross-shard query
       "Sharded": true
     },
     "FieldQuery": "select id, count(*) as c from `user` where 1 != 1 group by id",
-    "Query": "select id, count(*) as c from `user` where id = 1 group by id having c = 10",
+    "Query": "select id, count(*) as c from `user` where id = 1 group by id having count(*) = 10",
     "Table": "`user`",
     "Values": [
       "INT64(1)"
@@ -3388,7 +3388,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a = 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a = 10",
+    "Predicate": ":0 = 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3423,7 +3423,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a = '1'",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a = '1'",
+    "Predicate": ":0 = '1'",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3458,7 +3458,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a != 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a != 10",
+    "Predicate": ":0 != 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3493,7 +3493,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a != '1'",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a != '1'",
+    "Predicate": ":0 != '1'",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3528,7 +3528,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a \u003e 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a \u003e 10",
+    "Predicate": ":0 \u003e 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3563,7 +3563,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a \u003e= 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a \u003e= 10",
+    "Predicate": ":0 \u003e= 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3598,7 +3598,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a \u003c 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a \u003c 10",
+    "Predicate": ":0 \u003c 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3633,7 +3633,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a \u003c= 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a \u003c= 10",
+    "Predicate": ":0 \u003c= 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3668,7 +3668,7 @@ Gen4 plan same as above
   "Original": "select col, count(*) a from user group by col having a \u003c= 10",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a \u003c= 10",
+    "Predicate": ":1 \u003c= 10",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
@@ -3712,7 +3712,7 @@ Gen4 plan same as above
     "Inputs": [
       {
         "OperatorType": "Filter",
-        "Predicate": "a = 1.00",
+        "Predicate": ":0 = 1.00",
         "Inputs": [
           {
             "OperatorType": "Aggregate",
@@ -4913,7 +4913,7 @@ Gen4 error: aggregate functions take a single argument 'count(distinct user_id,
     "Inputs": [
       {
         "OperatorType": "Filter",
-        "Predicate": "fooSum + :2 = 42",
+        "Predicate": ":1 + :2 = 42",
         "Inputs": [
           {
             "OperatorType": "Aggregate",
diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
index 84b53c4464..83edf5118f 100644
--- a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
@@ -5938,6 +5938,33 @@ Gen4 plan same as above
 }
 Gen4 plan same as above
 
+# conditions following a null safe comparison operator can be used for routing
+"SELECT music.id FROM music LEFT OUTER JOIN user ON music.user_id = user.id WHERE user.id <=> NULL AND music.user_id = 10"
+{
+  "QueryType": "SELECT",
+  "Original": "SELECT music.id FROM music LEFT OUTER JOIN user ON music.user_id = user.id WHERE user.id \u003c=\u003e NULL AND music.user_id = 10",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select music.id from music left join `user` on music.user_id = `user`.id where 1 != 1",
+    "Query": "select music.id from music left join `user` on music.user_id = `user`.id where music.user_id = 10 and `user`.id \u003c=\u003e null",
+    "Table": "`user`, music",
+    "Values": [
+      "INT64(10)"
+    ],
+    "Vindex": "user_index"
+  },
+  "TablesUsed": [
+    "user.music",
+    "user.user"
+  ]
+}
+Gen4 plan same as above
+
 # optimize ORs to IN route op codes #1
 "select col from user where id = 1 or id = 2"
 {
diff --git a/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt b/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
index a18d6c4e53..9e96746f5b 100644
--- a/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
@@ -3027,7 +3027,7 @@ Gen4 plan same as above
   "Original": "select count(*) a from user having a = 0x01",
   "Instructions": {
     "OperatorType": "Filter",
-    "Predicate": "a = 0x01",
+    "Predicate": ":0 = 0x01",
     "Inputs": [
       {
         "OperatorType": "Aggregate",
diff --git a/go/vt/vtgate/planbuilder/testdata/select_cases.txt b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
index 635d6a89e4..bebdde5382 100644
--- a/go/vt/vtgate/planbuilder/testdata/select_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
@@ -4366,7 +4366,7 @@ Gen4 plan same as above
       "Sharded": false
     },
     "FieldQuery": "select 1 from (select col, count(*) as a from unsharded where 1 != 1 group by col) as f left join unsharded as u on f.col = u.id where 1 != 1",
-    "Query": "select 1 from (select col, count(*) as a from unsharded group by col having a \u003e 0 limit 0, 12) as f left join unsharded as u on f.col = u.id",
+    "Query": "select 1 from (select col, count(*) as a from unsharded group by col having count(*) \u003e 0 limit 0, 12) as f left join unsharded as u on f.col = u.id",
     "Table": "unsharded"
   },
   "TablesUsed": [
diff --git a/go/vt/vtgate/semantics/analyzer_test.go b/go/vt/vtgate/semantics/analyzer_test.go
index cfb7d0d229..f7bfe3b04f 100644
--- a/go/vt/vtgate/semantics/analyzer_test.go
+++ b/go/vt/vtgate/semantics/analyzer_test.go
@@ -812,6 +812,9 @@ func TestHavingBinding(t *testing.T) {
 	}, {
 		"select t.id, count(*) as a from t, t1 group by t.id having a = 1",
 		MergeTableSets(T1, T2),
+	}, {
+		"select t.id, sum(t2.name) as a from t, t2 group by t.id having a = 1",
+		T2,
 	}, {
 		sql:  "select u2.a, u1.a from u1, u2 having u2.a = 2",
 		deps: T2,
diff --git a/go/vt/vtgate/semantics/early_rewriter_test.go b/go/vt/vtgate/semantics/early_rewriter_test.go
index bb9642a57e..9b40227af9 100644
--- a/go/vt/vtgate/semantics/early_rewriter_test.go
+++ b/go/vt/vtgate/semantics/early_rewriter_test.go
@@ -145,7 +145,7 @@ func TestExpandStar(t *testing.T) {
 		expSQL: "select t1.b as b, t1.a as a, t1.c as c, t5.a as a from t1 join t5 where t1.b = t5.b",
 	}, {
 		sql:    "select * from t1 join t5 using (b) having b = 12",
-		expSQL: "select t1.b as b, t1.a as a, t1.c as c, t5.a as a from t1 join t5 where t1.b = t5.b having b = 12",
+		expSQL: "select t1.b as b, t1.a as a, t1.c as c, t5.a as a from t1 join t5 where t1.b = t5.b having t1.b = 12",
 	}, {
 		sql:    "select 1 from t1 join t5 using (b) having b = 12",
 		expSQL: "select 1 from t1 join t5 where t1.b = t5.b having t1.b = 12",
@@ -305,6 +305,35 @@ func TestOrderByGroupByLiteral(t *testing.T) {
 	}
 }
 
+func TestHavingByColumnName(t *testing.T) {
+	schemaInfo := &FakeSI{
+		Tables: map[string]*vindexes.Table{},
+	}
+	cDB := "db"
+	tcases := []struct {
+		sql    string
+		expSQL string
+		expErr string
+	}{{
+		sql:    "select id, sum(foo) as sumOfFoo from t1 having sumOfFoo > 1",
+		expSQL: "select id, sum(foo) as sumOfFoo from t1 having sum(foo) > 1",
+	}}
+	for _, tcase := range tcases {
+		t.Run(tcase.sql, func(t *testing.T) {
+			ast, err := sqlparser.Parse(tcase.sql)
+			require.NoError(t, err)
+			selectStatement := ast.(*sqlparser.Select)
+			_, err = Analyze(selectStatement, cDB, schemaInfo)
+			if tcase.expErr == "" {
+				require.NoError(t, err)
+				assert.Equal(t, tcase.expSQL, sqlparser.String(selectStatement))
+			} else {
+				require.EqualError(t, err, tcase.expErr)
+			}
+		})
+	}
+}
+
 func TestSemTableDependenciesAfterExpandStar(t *testing.T) {
 	schemaInfo := &FakeSI{Tables: map[string]*vindexes.Table{
 		"t1": {
diff --git a/test/ci_workflow_gen.go b/test/ci_workflow_gen.go
index 06e978a81b..23e43fa2da 100644
--- a/test/ci_workflow_gen.go
+++ b/test/ci_workflow_gen.go
@@ -69,11 +69,13 @@ var (
 	// Hence, they are not listed in the list below.
 	clusterList = []string{
 		"vtctlbackup_sharded_clustertest_heavy",
+		"12",
 		"13",
 		"ers_prs_newfeatures_heavy",
 		"15",
 		"vtgate_general_heavy",
 		"vtbackup_transform",
+		"18",
 		"xb_backup",
 		"21",
 		"22",
@@ -127,8 +129,6 @@ var (
 	}
 
 	clusterSelfHostedList = []string{
-		"12",
-		"18",
 		"vtorc",
 	}
 	clusterDockerList           = []string{}