Repository: vitessio/vitess
Pull request #11460 (2022-10-11 08:32:56)

vitess Online DDL atomic cut-over
## Description


This is work in progress for an atomic `RENAME` (not two-step gaping hole) in the cut-over process of a vitess/vreplication Online DDL migration.

Currently, we run a [two-step rename](https://vitess.io/blog/2022-04-06-online-ddl-vitess-cut-over/), when the original table is renamed away, and then a 2nd rename moves the `vreplication` table in its place. This is protected under a buffering rule on the primary, but replicas see two distinct renames and there is a point in time on the replica, where the table does not exist. Of course this means queries are failing.
There is also a scenario depicted in https://github.com/vitessio/vitess/issues/11226, where even queries against the primary may fail.

This PR introduces an algorithm similar to the `gh-ost` cut-over, described in http://code.openark.org/blog/mysql/solving-the-non-atomic-table-swap-take-iii-making-it-atomic and in https://github.com/github/gh-ost/issues/82

We still retain our query buffering logic, which protects is against existing logic failures in the `gh-ost` implementation, per https://github.com/github/gh-ost/issues/887.

the new logic to be described in detail once the PR is validated and considered safe. Some noteworthy details in the meantime:

- We introduce a sentry table, which means VReplication needs to know about it, even though its schema is immaterial. We therefore need to run `VReplicationWaitForPos` after creating the sentry table, which increases the overall cut-over process time. This does not affect incoming traffic, as the creation of the table comes before the critical section of locking/blocking/renaming. It does, however, lock the executor's mutex, which potentially adds a few more seconds to an already a few seconds worth of process. The impact is internal and of no real concern.
- We remove the `stowaway_table` logic. The swap is atomic, and there is no need to recover a "stowaway scenario". We will keep the recovery logic for backwards compatibility for one full release, then remove it. The recovery logic is found here: https://github.com/vitessio/vitess/blob/893cc9d7b6f80eab3b103ec327330ad501e16e4f/go/vt/vttablet/onlineddl/executor.go#L3258-L3280
- We introduce a `stage` column to `_vt.schema_migrations`. It serves multiple purposes:
  - we populate the `stage` column in every step of the cut-over logic, so that we can easily see where a failure took place
  - we audit every such change to `stage` in the logs, so we have a full history
  - most importantly, we use it to generate a GTID entry post `LOCK TABLES` and `RENAME TABLES`: the `LOCK TABLES` query does not generate a GTID entry. The `RENAME TABLES` query is blocked due to `LOCK TABLES` and of course does not generate a GTID entry. We update `stage` column immediately after issuing both, and thus ensure we have a post-lock/rename position for which we can wait for in `VReplicationWaitForPos`. 



## Related Issue(s)

- Supersedes #11418
- #6926
- #11226 

<!-- List related issues and pull requests. If this PR fixes an issue, please add it using Fixes #????  -->

## Checklist

-   [ ] "Backport me!" label has been added if this change should be backported
-   [ ] Tests were added or are not required
-   [ ] 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:
- [ ] Is it really necessary to add this flag?
- [ ] Flag names should be clear and intuitive (as far as possible)
- [ ] Help text should be descriptive.
- [ ] Flag names should use dashes (`-`) as word separators rather than underscores (`_`).

#### If a workflow is added or modified:
- [ ] Each item in `Jobs` should be named in order to mark it as `required`.
- [ ] If the workflow should be required, the maintainer team should be notified.

#### Bug fixes
- [ ] There should be at least one unit or end-to-end test.
- [ ] The Pull Request description should include a link to an issue that describes the bug.

#### 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.
- [ ] RPC changes should be compatible with vitess-operator
- [ ] If a flag is removed, then it should also be removed from VTop, if used there.

shlomi-noach: # A walkthrough to explain the new cut-over logic and why it is implemented the way it is.

First, to explain the current logic, also discussed in [this blog post](https://vitess.io/blog/2022-04-06-online-ddl-vitess-cut-over/).

## Current cut-over logic

Assume we modify table `t`. The changes actually take place on `t_vrepl`:

```
                 t             t_vrepl
```

When `t_vrepl` is in near sync with `t`, we place a quey buffering rule on table `t`, such that new queries on `t` are buffered. While buffering takes place, we do a two-step rename:

1. `rename table t to t_placeholder`, leaving a puncture in our database: `t` does not exist

```
t_placeholder    [ ]            t_vrepl
```

2. complete any final changes to `t_vrepl` and then `rename t_vrepl to t, t_placeholder to t_vrepl`

```
                  t             t_vrepl
```

In essence, we have swapped `t` and `t_vrepl`. But in the process, there is a point in time where `t` does not exist. Queries should not be aware of that thanks to the query buffering. However, there is no buffering on the replicas, and inconspicuous queries running on replicas, such as `select * from t limit 1` might return with a `table t does not exist` eror.

Renaming `t` away serves two purposes:

- An implicit block while any pending queries on `t` complete execution
- An absolute guarantee that no new queries operate on `t` afterwards

## New cut-over logic in this PR

We want to avoid the puncture, such that replicas always have `t` available. Essentially, this means we must run something like `rename table t to t_placeholder, t_vrepl to t` (we in fact run something more elaborate). However, since `vreplication` online DDL is asynchronous, we can't _just_ run this query. We have to ensure we apply any remaining binary log queries that operate on `t` onto `t_vrepl`.

The query buffering _should_ help us: activating the buffering means no queries should operate on `t`. However, it's not a full guarantee. The buffering only works for queries coming through `vtgate`, and they do not apply to queries coming from elsewhere. "elsewhere" could mean any local automation users may place, any monitoring/introspection queries, etc.

We therefore employ a variation of `gh-ost`'s [cut-over algorithm](https://github.com/github/gh-ost/issues/82), which, together with the query buffering, offers stronger guarantees. We first describe a _naive_ implementation, then point out where it can fail.

### In the naive implementation

1. We place a lock on `t` via `lock tables t write`
2. We apply any remaining entries from the binary log onto `t_vrepl`
3. We follow by a `rename table t to t_placeholder, t_vrepl to t`.

Problem is this won't work as expected. (1) and (3) can't both run from the same connection. If they were, (3) would fail because the connection did not lock `t_vrepl`. But if the connection _were_ to lock `t_vrepl` in (1) then `vreplication` would not be able to apply anything to that table. And we can't let `vreplication` own that connection because the Online DDL executor operates on a different place than `vreplication`, even if both run on the same server.

So (1) and (3) must run by different connections. But this poses a risk. When we run (3), how do we _ensure_ that (1) is still holding the lock? Maybe (1)'s connections was terminated, releasing the lock? Meaning queries are actually able to run against `t`, and so running (3) would cause corruption?

This is how `gh-ost` cut-over logic came to be. It places protection mechanims so that the `rename` can't take place unless everything is in perfect order. 

### Non-naive, simplified implementation

1. Create a sentry table `t_sentry`
2. A connection, call it "lock conn", issues `lock tables t_sentry write, t write`
3. A 2nd connection, call it "rename conn" issues `rename table t to t_sentry, t_vrepl to t`. This query blocks due to the `lock` query. However, if the `lock` query is accidentally killed, the `rename` fails, because `t_sentry` exists.
4. The `rename` does not block writes to `t_vrepl` because it's locked on renaming `t`. `vreplication` is able to complete processing the backlog.
5. We issue a `VReplicationWaitForPos`: the two tables are now in sync.
6. The lock conn issues a `drop table t_sentry`.
7. lock conn issues a `unlock tables`
8. This releases the `rename` query, which is able to swap the tables
9. done

### Nuances

- We create a `sentry` table. This confuses `vreplication` because it does not know the table. It must be told about the table because we `lock` it. We therefore make an initial `VReplicationWaitForPos` right after creating the table and before placing locks.
- in step (3), we wait until the `rename` is found to be running in MySQL. The `rename` takes precedence over any DML that may happen to be queued on the `LOCK`.
- in step (5) we issue a `VReplicationWaitForPos` to ensure the tables are in sync. But, to which GTID do we wait for? The statement `LOCK TABLES ...` does not generate a GTID entry. The `rename ...` is blocked and therefore also does not generate a GTID entry. We therefore inject an intentional transaction after the `rename` is blocked. We update the `_vt.schema_migrations` `stage` column. Doesn't matter exactly what we update, as long as we generate a GTID. We know that this GTID is made after `t` is completely blocked to writes.
- The `rename` is actually a full swap: `rename table t to t_sentry, t_vrepl to t, t_sentry to t_vrepl`

shlomi-noach: Not even sure the CI issue is related to this PR. Still looking.
shlomi-noach: From an internal discussion it seems the aforementioned CI problem is colossal, and not specifically related to this PR.
shlomi-noach: Ready to review! Please make sure to read the original comment and then also https://github.com/vitessio/vitess/pull/11460#issuecomment-1275826833, if you want to have a good grasp of what's going on. Hint: it's complex!
shlomi-noach: > Do we have a test that reproduces the problem that is described in the PR, or is that too difficult (needs to be timed precisely)?

Yeah, it's too difficult I think; and on top of this, because we do use query buffering on top, I'm not sure I _can_ reproduce it.

Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 3dadefe6e6..7405fe7f21 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,45 +1,46 @@
 * @deepthi
-bootstrap.sh @deepthi
-/.github/ISSUE_TEMPLATE/ @frouioui
-/.github/workflows/ @frouioui
+bootstrap.sh @ajm188 @deepthi @frouioui @vmg
+/.github/ISSUE_TEMPLATE/ @deepthi @frouioui @mattlord
+/.github/workflows/ @deepthi @frouioui @mattlord
 /config/mycnf/ @deepthi @shlomi-noach @mattlord
-/docker/ @derekperkins @dkhenry @mattlord
+/doc/ @deepthi @frouioui @GuptaManan100 @rsajwani
+/docker/ @deepthi @derekperkins @dkhenry @mattlord
 /examples/compose @shlomi-noach @GuptaManan100 @frouioui
-/examples/demo @sougou @mattlord
+/examples/demo @mattlord @rohit-nayak-ps
 /examples/local @rohit-nayak-ps @frouioui @mattlord @GuptaManan100
-/examples/operator @GuptaManan100 @frouioui
-/examples/region_sharding @deepthi
+/examples/operator @GuptaManan100 @frouioui @mattlord
+/examples/region_sharding @deepthi @mattlord
 /java/ @harshit-gangal
 /go/cache @vmg
-/go/cmd @ajm188
-/go/cmd/vtadmin @ajm188 @doeg @notfelineit
+/go/cmd @ajm188 @deepthi
+/go/cmd/vtadmin @ajm188 @notfelineit
 /go/cmd/vtctldclient @ajm188 @notfelineit
-/go/internal/flag @ajm188
+/go/internal/flag @ajm188 @rohit-nayak-ps
 /go/mysql @harshit-gangal @systay @mattlord
-/go/pools @harshit-gangal
-/go/protoutil @ajm188
-/go/sqltypes @harshit-gangal
-/go/test/endtoend/onlineddl @shlomi-noach
+/go/pools @deepthi @harshit-gangal
+/go/protoutil @ajm188 @deepthi
+/go/sqltypes @harshit-gangal @shlomi-noach @vmg
+/go/test/endtoend/onlineddl @rohit-nayak-ps @shlomi-noach
 /go/test/endtoend/messaging @mattlord @rohit-nayak-ps @derekperkins
 /go/test/endtoend/vtgate @harshit-gangal @systay @frouioui
 /go/test/endtoend/vtorc @deepthi @shlomi-noach @GuptaManan100 @rsajwani
 /go/tools/ @frouioui @systay
-/go/vt/dbconnpool @harshit-gangal
-/go/vt/discovery @deepthi
+/go/vt/dbconnpool @harshit-gangal @mattlord
+/go/vt/discovery @deepthi @frouioui
 /go/vt/mysqlctl @deepthi @mattlord @rsajwani
-/go/vt/proto @harshit-gangal
-/go/vt/proto/vtadmin @ajm188 @doeg @notfelineit
-/go/vt/schema @shlomi-noach
+/go/vt/proto @deepthi @harshit-gangal @mattlord
+/go/vt/proto/vtadmin @ajm188 @notfelineit
+/go/vt/schema @mattlord @shlomi-noach
 /go/vt/servenv @deepthi @ajm188
 /go/vt/sqlparser @harshit-gangal @systay @GuptaManan100
-/go/vt/srvtopo @rafael @mattlord
+/go/vt/srvtopo @deepthi @rafael @mattlord
 /go/vt/sysvars @harshit-gangal @systay
 /go/vt/topo @deepthi @rafael @mattlord @rsajwani
 /go/vt/topotools @deepthi @rafael @mattlord @rsajwani
 /go/vt/vitessdriver @harshit-gangal
-/go/vt/vtadmin @ajm188 @doeg @notfelineit @rohit-nayak-ps
-/go/vt/vtctl @deepthi
-/go/vt/vtctl/vtctl.go @ajm188 @notfelineit
+/go/vt/vtadmin @ajm188 @notfelineit @rohit-nayak-ps
+/go/vt/vtctl @ajm188 @deepthi @rohit-nayak-ps
+/go/vt/vtctl/vtctl.go @notfelineit @rohit-nayak-ps
 /go/vt/vtctl/grpcvtctldclient @ajm188 @notfelineit
 /go/vt/vtctl/grpcvtctldserver @ajm188 @notfelineit
 /go/vt/vtctl/reparentutil @ajm188 @GuptaManan100 @deepthi
@@ -49,18 +50,25 @@ bootstrap.sh @deepthi
 /go/vt/vtexplain @systay @harshit-gangal
 /go/vt/vtgate @harshit-gangal @systay @frouioui @GuptaManan100
 /go/vt/vtorc @deepthi @shlomi-noach @GuptaManan100 @rsajwani
+/go/vt/vttablet/*conn* @harshit-gangal @systay
+/go/vt/vttablet/endtoend @harshit-gangal @mattlord @rohit-nayak-ps @systay
+/go/vt/vttablet/grpc* @ajm188 @rohit-nayak-ps @rsajwani @shlomi-noach @harshit-gangal
+/go/vt/vttablet/onlineddl @mattlord @rohit-nayak-ps @shlomi-noach
 /go/vt/vttablet/queryservice @harshit-gangal @systay
-/go/vt/vttablet/tabletmanager @deepthi @shlomi-noach @rsajwani @rohit-nayak-ps
+/go/vt/vttablet/tabletmanager @deepthi @GuptaManan100 @rohit-nayak-ps @rsajwani @shlomi-noach
 /go/vt/vttablet/tabletmanager/vreplication @rohit-nayak-ps @mattlord
 /go/vt/vttablet/tabletmanager/vstreamer @rohit-nayak-ps @mattlord
-/go/vt/vttablet/tabletserver @harshit-gangal @systay @shlomi-noach @rohit-nayak-ps
+/go/vt/vttablet/tabletserver* @harshit-gangal @systay @shlomi-noach @rohit-nayak-ps
 /go/vt/vttablet/tabletserver/messager @mattlord @rohit-nayak-ps @derekperkins
-/go/vt/wrangler @deepthi @rohit-nayak-ps @mattlord
-/go/vt/workflow @rohit-nayak-ps @mattlord
-/proto/vtadmin.proto @ajm188 @doeg @notfelineit
+/go/vt/vttablet/*tmclient* @ajm188 @GuptaManan100 @rohit-nayak-ps @rsajwani @shlomi-noach
+/go/vt/vttablet/vexec @mattlord @rohit-nayak-ps @shlomi-noach
+/go/vt/wrangler @deepthi @mattlord @rohit-nayak-ps
+/go/vt/workflow @mattlord @rohit-nayak-ps
+/proto/ @deepthi @harshit-gangal
+/proto/vtadmin.proto @ajm188 @notfelineit
 /proto/vtctldata.proto @ajm188 @notfelineit
 /proto/vtctlservice.proto @ajm188 @notfelineit
 /test/ @GuptaManan100 @frouioui @rohit-nayak-ps
 /tools/ @frouioui @rohit-nayak-ps
-/web/vtadmin @ajm188 @doeg @notfelineit
+/web/vtadmin @ajm188 @notfelineit
 /web/vtctld2 @notfelineit @rohit-nayak-ps
diff --git a/.github/workflows/cluster_endtoend_26.yml b/.github/workflows/cluster_endtoend_26.yml
deleted file mode 100644
index 99253bde7f..0000000000
--- a/.github/workflows/cluster_endtoend_26.yml
+++ /dev/null
@@ -1,134 +0,0 @@
-# DO NOT MODIFY: THIS FILE IS GENERATED USING "make generate_ci_workflows"
-
-name: Cluster (26)
-on: [push, pull_request]
-concurrency:
-  group: format('{0}-{1}', ${{ github.ref }}, 'Cluster (26)')
-  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 (26)
-    runs-on: ubuntu-20.04
-
-    steps:
-    - name: Skip CI
-      run: |
-        if [[ "${{contains( github.event.pull_request.labels.*.name, 'Skip CI')}}" == "true" ]]; then
-          echo "skipping CI due to the 'Skip CI' label"
-          exit 1
-        fi
-
-    - 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@v3
-
-    - 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_26.yml'
-
-    - name: Set up Go
-      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-      uses: actions/setup-go@v3
-      with:
-        go-version: 1.18.7
-
-    - name: Set up python
-      if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
-      uses: actions/setup-python@v4
-
-    - 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: |
-        
-        # Get key to latest MySQL repo
-        sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
-        # Setup MySQL 8.0
-        wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.20-1_all.deb
-        echo mysql-apt-config mysql-apt-config/select-server select mysql-8.0 | sudo debconf-set-selections
-        sudo DEBIAN_FRONTEND="noninteractive" dpkg -i mysql-apt-config*
-        sudo apt-get update
-        # Install everything else we need, and configure
-        sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget eatmydata xz-utils
-
-        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: 45
-      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 26 | 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_vtbackup_transform.yml b/.github/workflows/cluster_endtoend_vtbackup.yml
similarity index 95%
rename from .github/workflows/cluster_endtoend_vtbackup_transform.yml
rename to .github/workflows/cluster_endtoend_vtbackup.yml
index 2a79f2817c..1ba7adf8bb 100644
--- a/.github/workflows/cluster_endtoend_vtbackup_transform.yml
+++ b/.github/workflows/cluster_endtoend_vtbackup.yml
@@ -1,9 +1,9 @@
 # DO NOT MODIFY: THIS FILE IS GENERATED USING "make generate_ci_workflows"
 
-name: Cluster (vtbackup_transform)
+name: Cluster (vtbackup)
 on: [push, pull_request]
 concurrency:
-  group: format('{0}-{1}', ${{ github.ref }}, 'Cluster (vtbackup_transform)')
+  group: format('{0}-{1}', ${{ github.ref }}, 'Cluster (vtbackup)')
   cancel-in-progress: true
 
 env:
@@ -13,7 +13,7 @@ env:
 
 jobs:
   build:
-    name: Run endtoend tests on Cluster (vtbackup_transform)
+    name: Run endtoend tests on Cluster (vtbackup)
     runs-on: ubuntu-20.04
 
     steps:
@@ -55,7 +55,7 @@ jobs:
             - 'tools/**'
             - 'config/**'
             - 'bootstrap.sh'
-            - '.github/workflows/cluster_endtoend_vtbackup_transform.yml'
+            - '.github/workflows/cluster_endtoend_vtbackup.yml'
 
     - name: Set up Go
       if: steps.skip-workflow.outputs.skip-workflow == 'false' && steps.changes.outputs.end_to_end == 'true'
@@ -122,7 +122,7 @@ jobs:
         set -x
 
         # run the tests however you normally do, then produce a JUnit XML file
-        eatmydata -- go run test.go -docker=false -follow -shard vtbackup_transform | tee -a output.txt | go-junit-report -set-exit-code > report.xml
+        eatmydata -- go run test.go -docker=false -follow -shard vtbackup | 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()
diff --git a/doc/releasenotes/14_0_0_release_notes.md b/doc/releasenotes/14_0_0_release_notes.md
index 50d02232ae..a7b4bcf4ef 100644
--- a/doc/releasenotes/14_0_0_release_notes.md
+++ b/doc/releasenotes/14_0_0_release_notes.md
@@ -18,6 +18,7 @@
 ## Known Issues
 
 - [VTOrc doesn't discover the tablets](https://github.com/vitessio/vitess/issues/10650) of a keyspace if the durability policy doesn't exist in the topo server when it comes up. This can be resolved by restarting VTOrc.
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
 
 ## Major Changes
 
diff --git a/doc/releasenotes/14_0_0_summary.md b/doc/releasenotes/14_0_0_summary.md
index e02c9d9a28..6047ec7ec1 100644
--- a/doc/releasenotes/14_0_0_summary.md
+++ b/doc/releasenotes/14_0_0_summary.md
@@ -17,6 +17,7 @@
 ## Known Issues
 
 - [VTOrc doesn't discover the tablets](https://github.com/vitessio/vitess/issues/10650) of a keyspace if the durability policy doesn't exist in the topo server when it comes up. This can be resolved by restarting VTOrc.
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
 
 ## Major Changes
 
diff --git a/doc/releasenotes/14_0_1_release_notes.md b/doc/releasenotes/14_0_1_release_notes.md
index 7215301939..57d68be31c 100644
--- a/doc/releasenotes/14_0_1_release_notes.md
+++ b/doc/releasenotes/14_0_1_release_notes.md
@@ -1,4 +1,8 @@
 # Release of Vitess v14.0.1
+## Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### Upgrade to `go1.18.4`
diff --git a/doc/releasenotes/14_0_1_summary.md b/doc/releasenotes/14_0_1_summary.md
index 1f2f985bab..5a176b6af1 100644
--- a/doc/releasenotes/14_0_1_summary.md
+++ b/doc/releasenotes/14_0_1_summary.md
@@ -1,3 +1,7 @@
+## Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### Upgrade to `go1.18.4`
diff --git a/doc/releasenotes/14_0_2_release_notes.md b/doc/releasenotes/14_0_2_release_notes.md
index 956ca21ef6..d83b940f45 100644
--- a/doc/releasenotes/14_0_2_release_notes.md
+++ b/doc/releasenotes/14_0_2_release_notes.md
@@ -1,4 +1,8 @@
 # Release of Vitess v14.0.2
+## Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### Upgrade to `go1.18.5`
diff --git a/doc/releasenotes/14_0_2_summary.md b/doc/releasenotes/14_0_2_summary.md
index 05a1aac5d6..8b26887dd0 100644
--- a/doc/releasenotes/14_0_2_summary.md
+++ b/doc/releasenotes/14_0_2_summary.md
@@ -1,3 +1,7 @@
+## Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### Upgrade to `go1.18.5`
diff --git a/doc/releasenotes/14_0_3_release_notes.md b/doc/releasenotes/14_0_3_release_notes.md
index d9cd4ac7a6..721c47640e 100644
--- a/doc/releasenotes/14_0_3_release_notes.md
+++ b/doc/releasenotes/14_0_3_release_notes.md
@@ -1,4 +1,8 @@
 # Release of Vitess v14.0.3
+## Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### Fix VTOrc Discovery
diff --git a/doc/releasenotes/14_0_3_summary.md b/doc/releasenotes/14_0_3_summary.md
index 9d9364f67b..121d9054a1 100644
--- a/doc/releasenotes/14_0_3_summary.md
+++ b/doc/releasenotes/14_0_3_summary.md
@@ -1,3 +1,7 @@
+## Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### Fix VTOrc Discovery
diff --git a/doc/releasenotes/15_0_0_release_notes.md b/doc/releasenotes/15_0_0_release_notes.md
index a67dbb9881..430100fa8f 100644
--- a/doc/releasenotes/15_0_0_release_notes.md
+++ b/doc/releasenotes/15_0_0_release_notes.md
@@ -1,6 +1,7 @@
 # Release of Vitess v15.0.0
 ## Summary
 
+- **[Known Issues](#known-issues)**
 - **[Breaking Changes](#breaking-changes)**
   - [Flags](#flags)
   - [VTTablet Flag Deletions](#vttablet-flag-deletions)
@@ -44,6 +45,10 @@
 - **[Flags Restructure](#flags-restructure)**
   - [Flags Diff](#flags-diff)
 
+## <a id="known-issues"/>Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### <a id="breaking-changes"/>Breaking Changes
diff --git a/doc/releasenotes/15_0_0_summary.md b/doc/releasenotes/15_0_0_summary.md
index 069f2c217b..56b784b55d 100644
--- a/doc/releasenotes/15_0_0_summary.md
+++ b/doc/releasenotes/15_0_0_summary.md
@@ -1,5 +1,6 @@
 ## Summary
 
+- **[Known Issues](#known-issues)**
 - **[Breaking Changes](#breaking-changes)**
   - [Flags](#flags)
   - [VTTablet Flag Deletions](#vttablet-flag-deletions)
@@ -43,6 +44,10 @@
 - **[Flags Restructure](#flags-restructure)**
   - [Flags Diff](#flags-diff)
 
+## <a id="known-issues"/>Known Issues
+
+- [Corrupted results for non-full-group-by queries with JOINs](https://github.com/vitessio/vitess/issues/11625). This can be resolved by using full-group-by queries.
+
 ## Major Changes
 
 ### <a id="breaking-changes"/>Breaking Changes
diff --git a/doc/releasenotes/16_0_0_summary.md b/doc/releasenotes/16_0_0_summary.md
index 8347d75b8c..41050fea6f 100644
--- a/doc/releasenotes/16_0_0_summary.md
+++ b/doc/releasenotes/16_0_0_summary.md
@@ -34,6 +34,13 @@ Other aspects of the VReplication copy-phase logic are preserved:
 
  Other phases, catchup, fast-forward, and replicating/"running", are unchanged.
 
+### Important bug fixes
+
+#### Corrupted results for non-full-group-by queries with JOINs
+
+An issue in versions `<= v14.0.3` and `<= v15.0.0` that generated corrupted results for non-full-group-by queries with a JOIN
+is now fixed. The full issue can be found [here](https://github.com/vitessio/vitess/issues/11625), and its fix [here](https://github.com/vitessio/vitess/pull/11633).
+
 ### Deprecations
 
 The V3 planner is deprecated as of the V16 release, and will be removed in the V17 release of Vitess.
diff --git a/go/ioutil2/ioutil.go b/go/ioutil2/ioutil.go
deleted file mode 100644
index 0b2ae7a645..0000000000
--- a/go/ioutil2/ioutil.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2019 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 ioutil2 provides extra functionality along similar lines to io/ioutil.
-package ioutil2
-
-import (
-	"os"
-	"path"
-)
-
-// WriteFileAtomic writes the data to a temp file and atomically move if everything else succeeds.
-func WriteFileAtomic(filename string, data []byte, perm os.FileMode) error {
-	dir, name := path.Split(filename)
-	f, err := os.CreateTemp(dir, name)
-	if err != nil {
-		return err
-	}
-	_, err = f.Write(data)
-	if err == nil {
-		err = f.Sync()
-	}
-	if closeErr := f.Close(); err == nil {
-		err = closeErr
-	}
-	if permErr := os.Chmod(f.Name(), perm); err == nil {
-		err = permErr
-	}
-	if err == nil {
-		err = os.Rename(f.Name(), filename)
-	}
-	// Any err should result in full cleanup.
-	if err != nil {
-		os.Remove(f.Name())
-	}
-	return err
-}
diff --git a/go/sqltypes/value.go b/go/sqltypes/value.go
index 20b8843b4b..cdb1ce9ccb 100644
--- a/go/sqltypes/value.go
+++ b/go/sqltypes/value.go
@@ -73,12 +73,12 @@ type (
 func NewValue(typ querypb.Type, val []byte) (v Value, err error) {
 	switch {
 	case IsSigned(typ):
-		if _, err := strconv.ParseInt(string(val), 0, 64); err != nil {
+		if _, err := strconv.ParseInt(string(val), 10, 64); err != nil {
 			return NULL, err
 		}
 		return MakeTrusted(typ, val), nil
 	case IsUnsigned(typ):
-		if _, err := strconv.ParseUint(string(val), 0, 64); err != nil {
+		if _, err := strconv.ParseUint(string(val), 10, 64); err != nil {
 			return NULL, err
 		}
 		return MakeTrusted(typ, val), nil
diff --git a/go/vt/schemadiff/table.go b/go/vt/schemadiff/table.go
index 3e0ba93432..e199738c44 100644
--- a/go/vt/schemadiff/table.go
+++ b/go/vt/schemadiff/table.go
@@ -814,7 +814,7 @@ func (c *CreateTableEntity) diffOptions(alterTable *sqlparser.AlterTable,
 			case "COLLATE":
 				// skip. the default collation is applied per CHARSET
 			case "COMMENT":
-				tableOption = &sqlparser.TableOption{String: ""}
+				tableOption = &sqlparser.TableOption{Value: sqlparser.NewStrLiteral("")}
 			case "COMPRESSION":
 				tableOption = &sqlparser.TableOption{Value: sqlparser.NewStrLiteral("")}
 			case "CONNECTION":
diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go
index 7c1aa2dfdb..2a53d1f68a 100644
--- a/go/vt/vtexplain/vtexplain_vtgate.go
+++ b/go/vt/vtexplain/vtexplain_vtgate.go
@@ -131,6 +131,14 @@ func (vte *VTExplain) buildTopology(opts *Options, vschemaStr string, ksShardMap
 		vte.explainTopo.KeyspaceShards[ks] = make(map[string]*topodatapb.ShardReference)
 
 		for _, shard := range shards {
+			// If the topology is in the middle of a reshard, there can be two shards covering the same key range (e.g.
+			// both source shard 80- and target shard 80-c0 cover the keyrange 80-c0). For the purposes of explain, we
+			// should only consider the one that is serving, hence we skip the ones not serving. Otherwise, vtexplain
+			// gives inconsistent results - sometimes it will route the query being explained to the source shard, and
+			// sometimes to the destination shard. See https://github.com/vitessio/vitess/issues/11632 .
+			if shardInfo, ok := ksShardMap[ks][shard.Name]; ok && !shardInfo.IsPrimaryServing {
+				continue
+			}
 			hostname := fmt.Sprintf("%s/%s", ks, shard.Name)
 			log.Infof("registering test tablet %s for keyspace %s shard %s", hostname, ks, shard.Name)
 
diff --git a/go/vt/vtgate/planbuilder/aggregation_pushing.go b/go/vt/vtgate/planbuilder/aggregation_pushing.go
index e677be660b..f6d72d2943 100644
--- a/go/vt/vtgate/planbuilder/aggregation_pushing.go
+++ b/go/vt/vtgate/planbuilder/aggregation_pushing.go
@@ -429,6 +429,10 @@ func isMinOrMax(in engine.AggregateOpcode) bool {
 	}
 }
 
+func isRandom(in engine.AggregateOpcode) bool {
+	return in == engine.AggregateRandom
+}
+
 func splitAggregationsToLeftAndRight(
 	ctx *plancontext.PlanningContext,
 	aggregations []operators.Aggr,
@@ -443,8 +447,8 @@ func splitAggregationsToLeftAndRight(
 		} else {
 			deps := ctx.SemTable.RecursiveDeps(aggr.Original.Expr)
 			var other *operators.Aggr
-			// if we are sending down min/max, we don't have to multiply the results with anything
-			if !isMinOrMax(aggr.OpCode) {
+			// if we are sending down min/max/random, we don't have to multiply the results with anything
+			if !isMinOrMax(aggr.OpCode) && !isRandom(aggr.OpCode) {
 				other = countStarAggr()
 			}
 			switch {
diff --git a/go/vt/vtgate/vindexes/lookup_internal.go b/go/vt/vtgate/vindexes/lookup_internal.go
index e1390fca75..2644c26140 100644
--- a/go/vt/vtgate/vindexes/lookup_internal.go
+++ b/go/vt/vtgate/vindexes/lookup_internal.go
@@ -39,7 +39,7 @@ var (
 	readLockNone      = "none"
 	readLockDefault   = readLockExclusive
 
-	readLockExprs map[string]string = map[string]string{
+	readLockExprs = map[string]string{
 		readLockExclusive: "for update",
 		readLockShared:    "lock in share mode",
 		readLockNone:      "",
diff --git a/go/vt/vttablet/onlineddl/executor.go b/go/vt/vttablet/onlineddl/executor.go
index 49df47d985..5233bc97f6 100644
--- a/go/vt/vttablet/onlineddl/executor.go
+++ b/go/vt/vttablet/onlineddl/executor.go
@@ -650,6 +650,22 @@ func (e *Executor) executeDirectly(ctx context.Context, onlineDDL *schema.Online
 	return acceptableErrorCodeFound, nil
 }
 
+// doesConnectionInfoMatch checks if theres a MySQL connection in PROCESSLIST whose Info matches given text
+func (e *Executor) doesConnectionInfoMatch(ctx context.Context, connID int64, submatch string) (bool, error) {
+	findProcessQuery, err := sqlparser.ParseAndBind(sqlFindProcess,
+		sqltypes.Int64BindVariable(connID),
+		sqltypes.StringBindVariable("%"+submatch+"%"),
+	)
+	if err != nil {
+		return false, err
+	}
+	rs, err := e.execQuery(ctx, findProcessQuery)
+	if err != nil {
+		return false, err
+	}
+	return len(rs.Rows) == 1, nil
+}
+
 // validateTableForAlterAction checks whether a table is good to undergo a ALTER operation. It returns detailed error if not.
 func (e *Executor) validateTableForAlterAction(ctx context.Context, onlineDDL *schema.OnlineDDL) (err error) {
 	// Validate table does not participate in foreign key relationship:
@@ -718,6 +734,10 @@ func (e *Executor) terminateVReplMigration(ctx context.Context, uuid string) err
 
 // cutOverVReplMigration stops vreplication, then removes the _vt.vreplication entry for the given migration
 func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) error {
+	if err := e.incrementCutoverAttempts(ctx, s.workflow); err != nil {
+		return err
+	}
+
 	tmClient := e.tabletManagerClient()
 	defer tmClient.Close()
 
@@ -739,31 +759,101 @@ func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) er
 		return err
 	}
 	isVreplicationTestSuite := onlineDDL.StrategySetting().IsVreplicationTestSuite()
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "starting cut-over")
+
+	var sentryTableName string
+
+	waitForPos := func(s *VReplStream, pos mysql.Position) error {
+		ctx, cancel := context.WithTimeout(ctx, vreplicationCutOverThreshold)
+		defer cancel()
+		// Wait for target to reach the up-to-date pos
+		if err := tmClient.VReplicationWaitForPos(ctx, tablet.Tablet, int(s.id), mysql.EncodePosition(pos)); err != nil {
+			return err
+		}
+		// Target is now in sync with source!
+		return nil
+	}
 
-	// A bit early on, we generate names for stowaway and temporary tables
-	// We do this here because right now we're in a safe place where nothing happened yet. If there's an error now, bail out
-	// and no harm done.
-	// Later on, when traffic is blocked and tables renamed, that's a more dangerous place to be in; we want as little logic
-	// in that place as possible.
-	var stowawayTableName string
 	if !isVreplicationTestSuite {
-		stowawayTableName, err = schema.GenerateGCTableName(schema.HoldTableGCState, newGCTableRetainTime())
+		// A bit early on, we generate names for stowaway and temporary tables
+		// We do this here because right now we're in a safe place where nothing happened yet. If there's an error now, bail out
+		// and no harm done.
+		// Later on, when traffic is blocked and tables renamed, that's a more dangerous place to be in; we want as little logic
+		// in that place as possible.
+		sentryTableName, err = schema.GenerateGCTableName(schema.HoldTableGCState, newGCTableRetainTime())
+		if err != nil {
+			return nil
+		}
+
+		// We create the sentry table before toggling writes, because this involves a WaitForPos, which takes some time. We
+		// don't want to overload the buffering time with this excessive wait.
+
+		if err := e.updateArtifacts(ctx, onlineDDL.UUID, sentryTableName); err != nil {
+			return err
+		}
+		parsed := sqlparser.BuildParsedQuery(sqlCreateSentryTable, sentryTableName)
+		if _, err := e.execQuery(ctx, parsed.Query); err != nil {
+			return err
+		}
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "sentry table created: %s", sentryTableName)
+
+		postSentryPos, err := e.primaryPosition(ctx)
 		if err != nil {
 			return err
 		}
-		// Audit stowawayTableName. If operation is complete, we remove the audit. But if this tablet fails while
-		// the original table is renamed (into stowaway table), then this will be both the evidence and the information we need
-		// to restore the table back into existence. This can (and will) be done by a different vttablet process
-		if err := e.updateMigrationStowawayTable(ctx, onlineDDL.UUID, stowawayTableName); err != nil {
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "waiting for post-sentry pos: %v", mysql.EncodePosition(postSentryPos))
+		if err := waitForPos(s, postSentryPos); err != nil {
 			return err
 		}
-		defer e.updateMigrationStowawayTable(ctx, onlineDDL.UUID, "")
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "post-sentry pos reached")
+	}
+
+	lockConn, err := e.pool.Get(ctx, nil)
+	if err != nil {
+		return err
+	}
+	defer lockConn.Recycle()
+	defer lockConn.Exec(ctx, sqlUnlockTables, 1, false)
+
+	renameConn, err := e.pool.Get(ctx, nil)
+	if err != nil {
+		return err
 	}
+	defer renameConn.Recycle()
+	defer renameConn.Kill("premature exit while renaming tables", 0)
+	renameQuery := sqlparser.BuildParsedQuery(sqlSwapTables, onlineDDL.Table, sentryTableName, vreplTable, onlineDDL.Table, sentryTableName, vreplTable)
+
+	waitForRenameProcess := func() error {
+		// This function waits until it finds the RENAME TABLE... query running in MySQL's PROCESSLIST, or until timeout
+		// The function assumes that one of the renamed tables is locked, thus causing the RENAME to block. If nothing
+		// is locked, then the RENAME will be near-instantaneious and it's unlikely that the function will find it.
+		renameWaitCtx, cancel := context.WithTimeout(ctx, vreplicationCutOverThreshold)
+		defer cancel()
+
+		for {
+			renameProcessFound, err := e.doesConnectionInfoMatch(renameWaitCtx, renameConn.ID(), "rename")
+			if err != nil {
+				return err
+			}
+			if renameProcessFound {
+				return nil
+			}
+			select {
+			case <-renameWaitCtx.Done():
+				return vterrors.Errorf(vtrpcpb.Code_ABORTED, "timeout for rename query: %s", renameQuery.Query)
+			case <-time.After(time.Second):
+				// sleep
+			}
+		}
+	}
+
+	renameCompleteChan := make(chan error)
 
 	bufferingCtx, bufferingContextCancel := context.WithCancel(ctx)
 	defer bufferingContextCancel()
 	// Preparation is complete. We proceed to cut-over.
 	toggleBuffering := func(bufferQueries bool) error {
+		log.Infof("toggling buffering: %t in migration %v", bufferQueries, onlineDDL.UUID)
 		e.toggleBufferTableFunc(bufferingCtx, onlineDDL.Table, bufferQueries)
 		if !bufferQueries {
 			// called after new table is in place.
@@ -774,29 +864,32 @@ func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) er
 				return err
 			}
 		}
+		log.Infof("toggled buffering: %t in migration %v", bufferQueries, onlineDDL.UUID)
 		return nil
 	}
+
 	var reenableOnce sync.Once
 	reenableWritesOnce := func() {
 		reenableOnce.Do(func() {
+			log.Infof("re-enabling writes in migration %v", onlineDDL.UUID)
 			toggleBuffering(false)
 			go log.Infof("cutOverVReplMigration %v: unbuffered queries", s.workflow)
 		})
 	}
-	go log.Infof("cutOverVReplMigration %v: buffering queries", s.workflow)
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "buffering queries")
 	// stop writes on source:
 	err = toggleBuffering(true)
 	defer reenableWritesOnce()
 	if err != nil {
 		return err
 	}
-
-	// swap out the table
 	// Give a fraction of a second for a scenario where a query is in
 	// query executor, it passed the ACLs and is _about to_ execute. This will be nicer to those queries:
 	// they will be able to complete before the rename, rather than block briefly on the rename only to find
 	// the table no longer exists.
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "graceful wait for buffering")
 	time.Sleep(100 * time.Millisecond)
+
 	if isVreplicationTestSuite {
 		// The testing suite may inject queries internally from the server via a recurring EVENT.
 		// Those queries are unaffected by query rules (ACLs) because they don't go through Vitess.
@@ -807,32 +900,41 @@ func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) er
 		if _, err := e.execQuery(ctx, parsed.Query); err != nil {
 			return err
 		}
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "test suite 'before' table renamed")
 	} else {
 		// real production
-		go log.Infof("cutOverVReplMigration %v: renaming table %v to %v", s.workflow, onlineDDL.Table, stowawayTableName)
-		parsed := sqlparser.BuildParsedQuery(sqlRenameTable, onlineDDL.Table, stowawayTableName)
-		if _, err := e.execQuery(ctx, parsed.Query); err != nil {
+
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "locking tables")
+		lockCtx, cancel := context.WithTimeout(ctx, vreplicationCutOverThreshold)
+		defer cancel()
+		lockTableQuery := sqlparser.BuildParsedQuery(sqlLockTwoTablesWrite, sentryTableName, onlineDDL.Table)
+		if _, err := lockConn.Exec(lockCtx, lockTableQuery.Query, 1, false); err != nil {
 			return err
 		}
-	}
 
-	// We have just created a gaping hole, the original table does not exist.
-	// we expect to fill that hole by swapping in the vrepl table. But if anything goes wrong we prepare
-	// to rename the table back:
-	defer func() {
-		if _, err := e.renameTableIfApplicable(ctx, stowawayTableName, onlineDDL.Table); err != nil {
-			vterrors.Errorf(vtrpcpb.Code_UNKNOWN, "cannot rename back swapped table: %v into %v: %v", stowawayTableName, onlineDDL.Table, err)
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "renaming tables")
+		go func() {
+			_, err := renameConn.Exec(ctx, renameQuery.Query, 1, false)
+			renameCompleteChan <- err
+		}()
+		// the rename should block, because of the LOCK. Wait for it to show up.
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "waiting for RENAME to block")
+		if err := waitForRenameProcess(); err != nil {
+			return err
 		}
-		go log.Infof("cutOverVReplMigration %v: restored table %v back to %v", s.workflow, stowawayTableName, onlineDDL.Table)
-	}()
-	// Right now: new queries are buffered, any existing query will have executed, and worst case scenario is
-	// that some leftover query finds the table is not actually there anymore...
-	// At any case, there's definitely no more writes to the table since it does not exist. We can
-	// safely take the (GTID) pos now.
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "RENAME found")
+	}
+
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "reading post-lock pos")
 	postWritesPos, err := e.primaryPosition(ctx)
 	if err != nil {
 		return err
 	}
+
+	// Right now: new queries are buffered, any existing query will have executed, and worst case scenario is
+	// that some leftover query finds the table is not actually there anymore...
+	// At any case, there's definitely no more writes to the table since it does not exist. We can
+	// safely take the (GTID) pos now.
 	_ = e.updateMigrationTimestamp(ctx, "liveness_timestamp", s.workflow)
 
 	// Writes are now disabled on table. Read up-to-date vreplication info, specifically to get latest (and fixed) pos:
@@ -841,22 +943,14 @@ func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) er
 		return err
 	}
 
-	waitForPos := func() error {
-		ctx, cancel := context.WithTimeout(ctx, 2*vreplicationCutOverThreshold)
-		defer cancel()
-		// Wait for target to reach the up-to-date pos
-		if err := tmClient.VReplicationWaitForPos(ctx, tablet.Tablet, int(s.id), mysql.EncodePosition(postWritesPos)); err != nil {
-			return err
-		}
-		// Target is now in sync with source!
-		return nil
-	}
-	go log.Infof("cutOverVReplMigration %v: waiting for position %v", s.workflow, mysql.EncodePosition(postWritesPos))
-	if err := waitForPos(); err != nil {
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "waiting for post-lock pos: %v", mysql.EncodePosition(postWritesPos))
+	if err := waitForPos(s, postWritesPos); err != nil {
+		e.updateMigrationStage(ctx, onlineDDL.UUID, "timeout while waiting for post-lock pos: %v", err)
 		return err
 	}
 	go log.Infof("cutOverVReplMigration %v: done waiting for position %v", s.workflow, mysql.EncodePosition(postWritesPos))
 	// Stop vreplication
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "stopping vreplication")
 	if _, err := e.vreplicationExec(ctx, tablet.Tablet, binlogplayer.StopVReplication(uint32(s.id), "stopped for online DDL cutover")); err != nil {
 		return err
 	}
@@ -871,24 +965,43 @@ func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) er
 			if _, err := e.execQuery(ctx, parsed.Query); err != nil {
 				return err
 			}
+			e.updateMigrationStage(ctx, onlineDDL.UUID, "test suite 'after' table renamed")
 		} else {
-			// Normal (non-testing) alter table
-			conn, err := dbconnpool.NewDBConnection(ctx, e.env.Config().DB.DbaWithDB())
-			if err != nil {
+			e.updateMigrationStage(ctx, onlineDDL.UUID, "validating rename is still in place")
+			if err := waitForRenameProcess(); err != nil {
 				return err
 			}
-			defer conn.Close()
 
-			parsed := sqlparser.BuildParsedQuery(sqlRenameTwoTables,
-				vreplTable, onlineDDL.Table,
-				stowawayTableName, vreplTable,
-			)
-			go log.Infof("cutOverVReplMigration %v: switch of tables %v, %v, %v", s.workflow, vreplTable, onlineDDL.Table, stowawayTableName)
-			if _, err := e.execQuery(ctx, parsed.Query); err != nil {
-				return err
+			// Normal (non-testing) alter table
+			e.updateMigrationStage(ctx, onlineDDL.UUID, "dropping sentry table")
+
+			{
+				dropTableQuery := sqlparser.BuildParsedQuery(sqlDropTable, sentryTableName)
+				lockCtx, cancel := context.WithTimeout(ctx, vreplicationCutOverThreshold)
+				defer cancel()
+				if _, err := lockConn.Exec(lockCtx, dropTableQuery.Query, 1, false); err != nil {
+					return err
+				}
+			}
+			{
+				lockCtx, cancel := context.WithTimeout(ctx, vreplicationCutOverThreshold)
+				defer cancel()
+				e.updateMigrationStage(ctx, onlineDDL.UUID, "unlocking tables")
+				if _, err := lockConn.Exec(lockCtx, sqlUnlockTables, 1, false); err != nil {
+					return err
+				}
+			}
+			{
+				lockCtx, cancel := context.WithTimeout(ctx, vreplicationCutOverThreshold)
+				defer cancel()
+				e.updateMigrationStage(lockCtx, onlineDDL.UUID, "waiting for RENAME to complete")
+				if err := <-renameCompleteChan; err != nil {
+					return err
+				}
 			}
 		}
 	}
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "cut-over complete")
 	e.ownedRunningMigrations.Delete(onlineDDL.UUID)
 
 	go func() {
@@ -903,13 +1016,13 @@ func (e *Executor) cutOverVReplMigration(ctx context.Context, s *VReplStream) er
 	}()
 
 	// Tables are now swapped! Migration is successful
+	e.updateMigrationStage(ctx, onlineDDL.UUID, "re-enabling writes")
 	reenableWritesOnce() // this function is also deferred, in case of early return; but now would be a good time to resume writes, before we publish the migration as "complete"
 	go log.Infof("cutOverVReplMigration %v: marking as complete", s.workflow)
 	_ = e.onSchemaMigrationStatus(ctx, onlineDDL.UUID, schema.OnlineDDLStatusComplete, false, progressPctFull, etaSecondsNow, s.rowsCopied, emptyHint)
 	return nil
 
 	// deferred function will re-enable writes now
-	// deferred function will unlock keyspace
 }
 
 // initMigrationSQLMode sets sql_mode according to DDL strategy, and returns a function that
@@ -943,6 +1056,53 @@ func (e *Executor) initMigrationSQLMode(ctx context.Context, onlineDDL *schema.O
 	return deferFunc, nil
 }
 
+// newConstraintName generates a new, unique name for a constraint. Our problem is that a MySQL
+// constraint's name is unique in the schema (!). And so as we duplicate the original table, we must
+// create completely new names for all constraints.
+// Moreover, we really want this name to be consistent across all shards. We therefore use a deterministic
+// UUIDv5 (SHA) function over the migration UUID, table name, and constraint's _contents_.
+// We _also_ include the original constraint name as prefix, as room allows
+// for example, if the original constraint name is "check_1",
+// we might generate "check_1_cps1okb4uafunfqusi2lp22u3".
+// If we then again migrate a table whose constraint name is "check_1_cps1okb4uafunfqusi2lp22u3	" we
+// get for example "check_1_19l09s37kbhj4axnzmi10e18k" (hash changes, and we still try to preserve original name)
+//
+// Furthermore, per bug report https://bugs.mysql.com/bug.php?id=107772, if the user doesn't provide a name for
+// their CHECK constraint, then MySQL picks a name in this format <tablename>_chk_<number>.
+// Example: sometable_chk_1
+// Next, when MySQL is asked to RENAME TABLE and sees a constraint with this format, it attempts to rename
+// the constraint with the new table's name. This is problematic for Vitess, because we often rename tables to
+// very long names, such as _vt_HOLD_394f9e6dfc3d11eca0390a43f95f28a3_20220706091048.
+// As we rename the constraint to e.g. `sometable_chk_1_cps1okb4uafunfqusi2lp22u3`, this makes MySQL want to
+// call the new constraint something like _vt_HOLD_394f9e6dfc3d11eca0390a43f95f28a3_20220706091048_chk_1_cps1okb4uafunfqusi2lp22u3,
+// which exceeds the 64 character limit for table names. Long story short, we also trim down <tablename> if the constraint seems
+// to be auto-generated.
+func (e *Executor) newConstraintName(onlineDDL *schema.OnlineDDL, hashExists map[string]bool, seed string, oldName string) string {
+	oldName = schemadiff.ExtractConstraintOriginalName(oldName)
+	autoGeneratedName := fmt.Sprintf("%s_chk_", onlineDDL.Table)
+	if strings.HasPrefix(oldName, autoGeneratedName) {
+		// strip out table name
+		oldName = "chk_" + oldName[len(autoGeneratedName):]
+	}
+
+	hash := textutil.UUIDv5Base36(onlineDDL.UUID, onlineDDL.Table, seed)
+	for i := 1; hashExists[hash]; i++ {
+		hash = textutil.UUIDv5Base36(onlineDDL.UUID, onlineDDL.Table, seed, fmt.Sprintf("%d", i))
+	}
+	hashExists[hash] = true
+	suffix := "_" + hash
+	maxAllowedNameLength := maxConstraintNameLength - len(suffix)
+	newName := oldName
+	if newName == "" {
+		newName = "chk" // start with something that looks consistent with MySQL's naming
+	}
+	if len(newName) > maxAllowedNameLength {
+		newName = newName[0:maxAllowedNameLength]
+	}
+	newName = newName + suffix
+	return newName
+}
+
 // validateAndEditCreateTableStatement inspects the CreateTable AST and does the following:
 // - extra validation (no FKs for now...)
 // - generate new and unique names for all constraints (CHECK and FK; yes, why not handle FK names; even as we don't support FKs today, we may in the future)
@@ -950,56 +1110,13 @@ func (e *Executor) validateAndEditCreateTableStatement(ctx context.Context, onli
 	constraintMap = map[string]string{}
 	hashExists := map[string]bool{}
 
-	// newConstraintName generates a new, unique name for a constraint. Our problem is that a MySQL
-	// constraint's name is unique in the schema (!). And so as we duplicate the original table, we must
-	// create completely new names for all constraints.
-	// Moreover, we really want this name to be consistent across all shards. We therefore use a deterministic
-	// UUIDv5 (SHA) function over the migration UUID, table name, and constraint's _contents_.
-	// We _also_ include the original constraint name as prefix, as room allows
-	// for example, if the original constraint name is "check_1",
-	// we might generate "check_1_cps1okb4uafunfqusi2lp22u3".
-	// If we then again migrate a table whose constraint name is "check_1_cps1okb4uafunfqusi2lp22u3	" we
-	// get for example "check_1_19l09s37kbhj4axnzmi10e18k" (hash changes, and we still try to preserve original name)
-	//
-	// Furthermore, per bug report https://bugs.mysql.com/bug.php?id=107772, if the user doesn' tprovide a name for
-	// their CHECK constraint, then MySQL picks a name in this format <tablename>_chk_<number>.
-	// Example: sometable_chk_1
-	// Next, when MySQL is asked to RENAME TABLE and sees a constraint with this format, it attempts to rename
-	// the constraint with the new table's name. This is problematic for Vitess, because we often rename tables to
-	// very long names, such as _vt_HOLD_394f9e6dfc3d11eca0390a43f95f28a3_20220706091048.
-	// As we rename the constraint to e.g. `sometable_chk_1_cps1okb4uafunfqusi2lp22u3`, this makes MySQL want to
-	// call the new constraint something like _vt_HOLD_394f9e6dfc3d11eca0390a43f95f28a3_20220706091048_chk_1_cps1okb4uafunfqusi2lp22u3,
-	// which exceeds the 64 character limit for table names. Long story short, we also trim down <tablename> if the constraint seems
-	// to be auto-generated.
-	newConstraintName := func(seed string, oldName string) string {
-		oldName = schemadiff.ExtractConstraintOriginalName(oldName)
-		autoGeneratedName := fmt.Sprintf("%s_chk_", onlineDDL.Table)
-		if strings.HasPrefix(oldName, autoGeneratedName) {
-			// strip out table name
-			oldName = "chk_" + oldName[len(autoGeneratedName):]
-		}
-
-		hash := textutil.UUIDv5Base36(onlineDDL.UUID, onlineDDL.Table, seed)
-		for i := 1; hashExists[hash]; i++ {
-			hash = textutil.UUIDv5Base36(onlineDDL.UUID, onlineDDL.Table, seed, fmt.Sprintf("%d", i))
-		}
-		hashExists[hash] = true
-		suffix := "_" + hash
-		maxAllowedNameLength := maxConstraintNameLength - len(suffix)
-		newName := oldName
-		if len(newName) > maxAllowedNameLength {
-			newName = newName[0:maxAllowedNameLength]
-		}
-		newName = newName + suffix
-		return newName
-	}
 	validateWalk := func(node sqlparser.SQLNode) (kontinue bool, err error) {
 		switch node := node.(type) {
 		case *sqlparser.ForeignKeyDefinition:
 			return false, schema.ErrForeignKeyFound
 		case *sqlparser.ConstraintDefinition:
 			oldName := node.Name.String()
-			newName := newConstraintName(sqlparser.CanonicalString(node.Details), oldName)
+			newName := e.newConstraintName(onlineDDL, hashExists, sqlparser.CanonicalString(node.Details), oldName)
 			node.Name = sqlparser.NewIdentifierCI(newName)
 			constraintMap[oldName] = newName
 		}
@@ -1014,7 +1131,8 @@ func (e *Executor) validateAndEditCreateTableStatement(ctx context.Context, onli
 // validateAndEditAlterTableStatement inspects the AlterTable statement and:
 // - modifies any CONSTRAINT name according to given name mapping
 // - explode ADD FULLTEXT KEY into multiple statements
-func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, alterTable *sqlparser.AlterTable, constraintMap map[string]string) (alters []*sqlparser.AlterTable, err error) {
+func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, onlineDDL *schema.OnlineDDL, alterTable *sqlparser.AlterTable, constraintMap map[string]string) (alters []*sqlparser.AlterTable, err error) {
+	hashExists := map[string]bool{}
 	validateWalk := func(node sqlparser.SQLNode) (kontinue bool, err error) {
 		switch node := node.(type) {
 		case *sqlparser.DropKey:
@@ -1026,6 +1144,11 @@ func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, alter
 				}
 				node.Name = sqlparser.NewIdentifierCI(mappedName)
 			}
+		case *sqlparser.AddConstraintDefinition:
+			oldName := node.ConstraintDefinition.Name.String()
+			newName := e.newConstraintName(onlineDDL, hashExists, sqlparser.CanonicalString(node.ConstraintDefinition.Details), oldName)
+			node.ConstraintDefinition.Name = sqlparser.NewIdentifierCI(newName)
+			constraintMap[oldName] = newName
 		}
 		return true, nil
 	}
@@ -1125,7 +1248,7 @@ func (e *Executor) initVreplicationOriginalMigration(ctx context.Context, online
 		// ALTER TABLE should apply to the vrepl table
 		alterTable.SetTable(alterTable.GetTable().Qualifier.CompliantName(), vreplTableName)
 		// Also, change any constraint names:
-		alters, err := e.validateAndEditAlterTableStatement(ctx, alterTable, constraintMap)
+		alters, err := e.validateAndEditAlterTableStatement(ctx, onlineDDL, alterTable, constraintMap)
 		if err != nil {
 			return v, err
 		}
@@ -3344,6 +3467,7 @@ func (e *Executor) reviewRunningMigrations(ctx context.Context) (countRunnning i
 					if isReady {
 						if err := e.cutOverVReplMigration(ctx, s); err != nil {
 							_ = e.updateMigrationMessage(ctx, uuid, err.Error())
+							log.Errorf("cutOverVReplMigration failed: err=%v", err)
 							if merr, ok := err.(*mysql.SQLError); ok {
 								switch merr.Num {
 								case mysql.ERTooLongIdent:
@@ -3780,6 +3904,31 @@ func (e *Executor) updateMigrationSpecialPlan(ctx context.Context, uuid string,
 	return err
 }
 
+func (e *Executor) updateMigrationStage(ctx context.Context, uuid string, stage string, args ...interface{}) error {
+	msg := fmt.Sprintf(stage, args...)
+	log.Infof("updateMigrationStage: uuid=%s, stage=%s", uuid, msg)
+	query, err := sqlparser.ParseAndBind(sqlUpdateStage,
+		sqltypes.StringBindVariable(msg),
+		sqltypes.StringBindVariable(uuid),
+	)
+	if err != nil {
+		return err
+	}
+	_, err = e.execQuery(ctx, query)
+	return err
+}
+
+func (e *Executor) incrementCutoverAttempts(ctx context.Context, uuid string) error {
+	query, err := sqlparser.ParseAndBind(sqlIncrementCutoverAttempts,
+		sqltypes.StringBindVariable(uuid),
+	)
+	if err != nil {
+		return err
+	}
+	_, err = e.execQuery(ctx, query)
+	return err
+}
+
 // updateMigrationTablet sets 'tablet' column to be this executor's tablet alias for given migration
 func (e *Executor) updateMigrationTablet(ctx context.Context, uuid string) error {
 	query, err := sqlparser.ParseAndBind(sqlUpdateTablet,
diff --git a/go/vt/vttablet/onlineddl/schema.go b/go/vt/vttablet/onlineddl/schema.go
index 3022b3736b..d288785139 100644
--- a/go/vt/vttablet/onlineddl/schema.go
+++ b/go/vt/vttablet/onlineddl/schema.go
@@ -81,6 +81,8 @@ const (
 	alterSchemaMigrationsComponentThrottled            = "ALTER TABLE _vt.schema_migrations add column component_throttled tinytext NOT NULL"
 	alterSchemaMigrationsCancelledTimestamp            = "ALTER TABLE _vt.schema_migrations add column cancelled_timestamp timestamp NULL DEFAULT NULL"
 	alterSchemaMigrationsTablePostponeLaunch           = "ALTER TABLE _vt.schema_migrations add column postpone_launch tinyint unsigned NOT NULL DEFAULT 0"
+	alterSchemaMigrationsStage                         = "ALTER TABLE _vt.schema_migrations add column stage text not null"
+	alterSchemaMigrationsCutoverAttempts               = "ALTER TABLE _vt.schema_migrations add column cutover_attempts int unsigned NOT NULL DEFAULT 0"
 
 	sqlInsertMigration = `INSERT IGNORE INTO _vt.schema_migrations (
 		migration_uuid,
@@ -202,6 +204,16 @@ const (
 		WHERE
 			migration_uuid=%a
 	`
+	sqlUpdateStage = `UPDATE _vt.schema_migrations
+			SET stage=%a
+		WHERE
+			migration_uuid=%a
+	`
+	sqlIncrementCutoverAttempts = `UPDATE _vt.schema_migrations
+			SET cutover_attempts=cutover_attempts+1
+		WHERE
+			migration_uuid=%a
+	`
 	sqlUpdateReadyForCleanup = `UPDATE _vt.schema_migrations
 			SET retain_artifacts_seconds=-1
 		WHERE
@@ -284,6 +296,8 @@ const (
 			retries=retries + 1,
 			tablet_failure=0,
 			message='',
+			stage='',
+			cutover_attempts=0,
 			ready_timestamp=NULL,
 			started_timestamp=NULL,
 			liveness_timestamp=NULL,
@@ -302,6 +316,8 @@ const (
 			retries=retries + 1,
 			tablet_failure=0,
 			message='',
+			stage='',
+			cutover_attempts=0,
 			ready_timestamp=NULL,
 			started_timestamp=NULL,
 			liveness_timestamp=NULL,
@@ -581,9 +597,12 @@ const (
 			_vt.copy_state
 		WHERE vrepl_id=%a
 		`
-	sqlSwapTables      = "RENAME TABLE `%a` TO `%a`, `%a` TO `%a`, `%a` TO `%a`"
-	sqlRenameTable     = "RENAME TABLE `%a` TO `%a`"
-	sqlRenameTwoTables = "RENAME TABLE `%a` TO `%a`, `%a` TO `%a`"
+	sqlSwapTables         = "RENAME TABLE `%a` TO `%a`, `%a` TO `%a`, `%a` TO `%a`"
+	sqlRenameTable        = "RENAME TABLE `%a` TO `%a`"
+	sqlLockTwoTablesWrite = "LOCK TABLES `%a` WRITE, `%a` WRITE"
+	sqlUnlockTables       = "UNLOCK TABLES"
+	sqlCreateSentryTable  = "CREATE TABLE IF NOT EXISTS `%a` (id INT PRIMARY KEY)"
+	sqlFindProcess        = "SELECT id, Info as info FROM information_schema.processlist WHERE id=%a AND Info LIKE %a"
 )
 
 const (
@@ -648,4 +667,6 @@ var ApplyDDL = []string{
 	alterSchemaMigrationsComponentThrottled,
 	alterSchemaMigrationsCancelledTimestamp,
 	alterSchemaMigrationsTablePostponeLaunch,
+	alterSchemaMigrationsStage,
+	alterSchemaMigrationsCutoverAttempts,
 }
diff --git a/web/vtadmin/package-lock.json b/web/vtadmin/package-lock.json
index a91a0ae0e1..433d25c170 100644
--- a/web/vtadmin/package-lock.json
+++ b/web/vtadmin/package-lock.json
@@ -5817,9 +5817,9 @@
       }
     },
     "node_modules/babel-loader/node_modules/loader-utils": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
-      "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.1.tgz",
+      "integrity": "sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q==",
       "dependencies": {
         "big.js": "^5.2.2",
         "emojis-list": "^3.0.0",
@@ -14747,9 +14747,9 @@
       }
     },
     "node_modules/loader-utils": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
-      "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
+      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
       "dependencies": {
         "big.js": "^5.2.2",
         "emojis-list": "^3.0.0",
@@ -25605,9 +25605,9 @@
           }
         },
         "loader-utils": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
-          "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.1.tgz",
+          "integrity": "sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q==",
           "requires": {
             "big.js": "^5.2.2",
             "emojis-list": "^3.0.0",
@@ -32099,9 +32099,9 @@
       "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw=="
     },
     "loader-utils": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
-      "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
+      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
       "requires": {
         "big.js": "^5.2.2",
         "emojis-list": "^3.0.0",


Test patch:
diff --git a/go/ioutil2/ioutil_test.go b/go/ioutil2/ioutil_test.go
deleted file mode 100644
index 916b1fe207..0000000000
--- a/go/ioutil2/ioutil_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright 2019 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 ioutil2
-
-import (
-	"bytes"
-	"fmt"
-	"os"
-	"testing"
-	"time"
-)
-
-func TestWrite(t *testing.T) {
-	data := []byte("test string\n")
-	fname := fmt.Sprintf("/tmp/atomic-file-test-%v.txt", time.Now().UnixNano())
-	err := WriteFileAtomic(fname, data, 0664)
-	if err != nil {
-		t.Fatal(err)
-	}
-	rData, err := os.ReadFile(fname)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !bytes.Equal(data, rData) {
-		t.Fatalf("data mismatch: %v != %v", data, rData)
-	}
-	if err := os.Remove(fname); err != nil {
-		t.Fatal(err)
-	}
-}
diff --git a/go/sqltypes/value_test.go b/go/sqltypes/value_test.go
index a6d91c814d..1e6c597bd9 100644
--- a/go/sqltypes/value_test.go
+++ b/go/sqltypes/value_test.go
@@ -86,6 +86,14 @@ func TestNewValue(t *testing.T) {
 		inType: Uint64,
 		inVal:  "1",
 		outVal: TestValue(Uint64, "1"),
+	}, {
+		inType: Uint64,
+		inVal:  "01",
+		outVal: TestValue(Uint64, "01"),
+	}, {
+		inType: Int64,
+		inVal:  "01",
+		outVal: TestValue(Int64, "01"),
 	}, {
 		inType: Float32,
 		inVal:  "1.00",
diff --git a/go/test/endtoend/backup/transform/backup_transform_test.go b/go/test/endtoend/backup/transform/backup_transform_test.go
deleted file mode 100644
index 071f7d536e..0000000000
--- a/go/test/endtoend/backup/transform/backup_transform_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
-Copyright 2019 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 transform
-
-import "testing"
-
-func TestMain(m *testing.M) {
-	TestMainSetup(m, false)
-}
-
-func TestBackupTransform(t *testing.T) {
-	TestBackupTransformImpl(t)
-}
-func TestBackupTransformError(t *testing.T) {
-	TestBackupTransformErrorImpl(t)
-}
diff --git a/go/test/endtoend/backup/transform/backup_transform_utils.go b/go/test/endtoend/backup/transform/backup_transform_utils.go
deleted file mode 100644
index 8418f7e999..0000000000
--- a/go/test/endtoend/backup/transform/backup_transform_utils.go
+++ /dev/null
@@ -1,395 +0,0 @@
-/*
-Copyright 2019 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 transform
-
-import (
-	"encoding/json"
-	"flag"
-	"fmt"
-	"os"
-	"os/exec"
-	"path"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"vitess.io/vitess/go/test/endtoend/cluster"
-	"vitess.io/vitess/go/vt/log"
-	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
-)
-
-// test main part of the testcase
-var (
-	primary          *cluster.Vttablet
-	replica1         *cluster.Vttablet
-	replica2         *cluster.Vttablet
-	testTablets      []*cluster.Vttablet
-	localCluster     *cluster.LocalProcessCluster
-	newInitDBFile    string
-	cell             = cluster.DefaultCell
-	hostname         = "localhost"
-	keyspaceName     = "ks"
-	dbPassword       = "VtDbaPass"
-	shardKsName      = fmt.Sprintf("%s/%s", keyspaceName, shardName)
-	dbCredentialFile string
-	shardName        = "0"
-	commonTabletArg  = []string{
-		"--vreplication_healthcheck_topology_refresh", "1s",
-		"--vreplication_healthcheck_retry_delay", "1s",
-		"--vreplication_retry_delay", "1s",
-		"--degraded_threshold", "5s",
-		"--lock_tables_timeout", "5s",
-		"--watch_replication_stream",
-		"--enable_replication_reporter",
-		"--serving_state_grace_period", "1s"}
-)
-
-// TestMainSetup sets up the basic test cluster
-func TestMainSetup(m *testing.M, useMysqlctld bool) {
-	defer cluster.PanicHandler(nil)
-	flag.Parse()
-
-	exitCode, err := func() (int, error) {
-		localCluster = cluster.NewCluster(cell, hostname)
-		defer localCluster.Teardown()
-
-		// Start topo server
-		err := localCluster.StartTopo()
-		if err != nil {
-			return 1, err
-		}
-
-		// Start keyspace
-		localCluster.Keyspaces = []cluster.Keyspace{
-			{
-				Name: keyspaceName,
-				Shards: []cluster.Shard{
-					{
-						Name: shardName,
-					},
-				},
-			},
-		}
-		shard := &localCluster.Keyspaces[0].Shards[0]
-		// changing password for mysql user
-		dbCredentialFile = cluster.WriteDbCredentialToTmp(localCluster.TmpDirectory)
-		initDb, _ := os.ReadFile(path.Join(os.Getenv("VTROOT"), "/config/init_db.sql"))
-		sql := string(initDb)
-		newInitDBFile = path.Join(localCluster.TmpDirectory, "init_db_with_passwords.sql")
-		sql = sql + cluster.GetPasswordUpdateSQL(localCluster)
-		os.WriteFile(newInitDBFile, []byte(sql), 0666)
-
-		extraArgs := []string{"--db-credentials-file", dbCredentialFile}
-		commonTabletArg = append(commonTabletArg, "--db-credentials-file", dbCredentialFile)
-
-		// start mysql process for all replicas and primary
-		var mysqlProcs []*exec.Cmd
-		for i := 0; i < 3; i++ {
-			tabletType := "replica"
-			tablet := localCluster.NewVttabletInstance(tabletType, 0, cell)
-			tablet.VttabletProcess = localCluster.VtprocessInstanceFromVttablet(tablet, shard.Name, keyspaceName)
-			tablet.VttabletProcess.DbPassword = dbPassword
-			tablet.VttabletProcess.ExtraArgs = commonTabletArg
-			tablet.VttabletProcess.SupportsBackup = true
-			tablet.VttabletProcess.EnableSemiSync = true
-
-			if useMysqlctld {
-				tablet.MysqlctldProcess = *cluster.MysqlCtldProcessInstance(tablet.TabletUID, tablet.MySQLPort, localCluster.TmpDirectory)
-				tablet.MysqlctldProcess.InitDBFile = newInitDBFile
-				tablet.MysqlctldProcess.ExtraArgs = extraArgs
-				tablet.MysqlctldProcess.Password = tablet.VttabletProcess.DbPassword
-				err := tablet.MysqlctldProcess.Start()
-				if err != nil {
-					return 1, err
-				}
-
-				shard.Vttablets = append(shard.Vttablets, tablet)
-				continue
-			}
-
-			tablet.MysqlctlProcess = *cluster.MysqlCtlProcessInstance(tablet.TabletUID, tablet.MySQLPort, localCluster.TmpDirectory)
-			tablet.MysqlctlProcess.InitDBFile = newInitDBFile
-			tablet.MysqlctlProcess.ExtraArgs = extraArgs
-			proc, err := tablet.MysqlctlProcess.StartProcess()
-			if err != nil {
-				return 1, err
-			}
-			mysqlProcs = append(mysqlProcs, proc)
-
-			shard.Vttablets = append(shard.Vttablets, tablet)
-		}
-		for _, proc := range mysqlProcs {
-			if err := proc.Wait(); err != nil {
-				return 1, err
-			}
-		}
-
-		// initialize tablets
-		primary = shard.Vttablets[0]
-		replica1 = shard.Vttablets[1]
-		replica2 = shard.Vttablets[2]
-		testTablets = []*cluster.Vttablet{primary, replica1, replica2}
-
-		for _, tablet := range testTablets {
-			if err := localCluster.VtctlclientProcess.InitTablet(tablet, cell, keyspaceName, hostname, shard.Name); err != nil {
-				return 1, err
-			}
-		}
-		vtctldClientProcess := cluster.VtctldClientProcessInstance("localhost", localCluster.VtctldProcess.GrpcPort, localCluster.TmpDirectory)
-		_, err = vtctldClientProcess.ExecuteCommandWithOutput("SetKeyspaceDurabilityPolicy", keyspaceName, "--durability-policy=semi_sync")
-		if err != nil {
-			return 1, err
-		}
-
-		// create database for primary and replicas
-		for _, tablet := range testTablets {
-			if err := tablet.VttabletProcess.CreateDB(keyspaceName); err != nil {
-				return 1, err
-			}
-			if err := tablet.VttabletProcess.Setup(); err != nil {
-				return 1, err
-			}
-		}
-
-		// initialize primary and start replication
-		if err := localCluster.VtctlclientProcess.InitShardPrimary(keyspaceName, shard.Name, cell, primary.TabletUID); err != nil {
-			return 1, err
-		}
-		primary.Type = topodatapb.TabletType_PRIMARY.String()
-		return m.Run(), nil
-	}()
-
-	if err != nil {
-		log.Error(err.Error())
-		os.Exit(1)
-	} else {
-		os.Exit(exitCode)
-	}
-}
-
-// create query for test table creation
-var vtInsertTest = `create table vt_insert_test (
-	id bigint auto_increment,
-	msg varchar(64),
-	primary key (id)
-	) Engine=InnoDB`
-
-// TestBackupTransformImpl tests backups with transform hooks
-func TestBackupTransformImpl(t *testing.T) {
-	// insert data in primary, validate same in replica
-	defer cluster.PanicHandler(t)
-	verifyInitialReplication(t)
-
-	// restart the replica1 tablet with transform hook parameter
-	replica1.VttabletProcess.TearDown()
-	replica1.VttabletProcess.ExtraArgs = []string{
-		"--db-credentials-file", dbCredentialFile,
-		"--backup_storage_hook", "test_backup_transform",
-		"--backup_storage_compress=false",
-		"--restore_from_backup",
-		"--backup_storage_implementation", "file",
-		"--file_backup_storage_root", localCluster.VtctldProcess.FileBackupStorageRoot}
-	replica1.VttabletProcess.ServingStatus = "SERVING"
-	err := replica1.VttabletProcess.Setup()
-	require.Nil(t, err)
-
-	if err := cluster.WaitForTabletSetup(&localCluster.VtctlclientProcess, 2, []string{"replica", "primary"}); err != nil {
-		require.Nil(t, err)
-	}
-
-	// take backup, it should not give any error
-	err = localCluster.VtctlclientProcess.ExecuteCommand("Backup", replica1.Alias)
-	require.Nil(t, err)
-
-	// insert data in primary
-	_, err = primary.VttabletProcess.QueryTablet("insert into vt_insert_test (msg) values ('test2')", keyspaceName, true)
-	require.Nil(t, err)
-
-	// validate backup_list, expecting 1 backup available
-	backups := localCluster.VerifyBackupCount(t, shardKsName, 1)
-
-	backupLocation := localCluster.CurrentVTDATAROOT + "/backups/" + shardKsName + "/" + backups[0]
-
-	// validate that MANIFEST has TransformHook
-	// every file should start with 'header'
-	validateManifestFile(t, backupLocation)
-
-	// restore replica2 from backup, should not give any error
-	// Note: we don't need to pass in the backup_storage_transform parameter,
-	// as it is read from the MANIFEST.
-	// clear replica2
-
-	// Stop the tablet
-	replica2.VttabletProcess.TearDown()
-	// Remove the data
-	if replica2.MysqlctlProcess.TabletUID > 0 {
-		err = replica2.MysqlctlProcess.Stop()
-		require.NoError(t, err)
-		replica2.MysqlctlProcess.CleanupFiles(replica2.TabletUID)
-		err = replica2.MysqlctlProcess.Start()
-		require.Nil(t, err)
-	} else {
-		err = replica2.MysqlctldProcess.Stop()
-		require.NoError(t, err)
-		replica2.MysqlctldProcess.CleanupFiles(replica2.TabletUID)
-		require.NoError(t, err)
-		err = replica2.MysqlctldProcess.Start()
-		require.Nil(t, err)
-	}
-
-	// Start from the backup
-	err = localCluster.VtctlclientProcess.InitTablet(replica2, cell, keyspaceName, hostname, shardName)
-	require.Nil(t, err)
-	replica2.VttabletProcess.ExtraArgs = []string{
-		"--db-credentials-file", dbCredentialFile,
-		"--restore_from_backup",
-		"--backup_storage_implementation", "file",
-		"--file_backup_storage_root", localCluster.VtctldProcess.FileBackupStorageRoot}
-	replica2.VttabletProcess.ServingStatus = ""
-	err = replica2.VttabletProcess.Setup()
-	require.Nil(t, err)
-	err = replica2.VttabletProcess.WaitForTabletStatusesForTimeout([]string{"SERVING"}, 30*time.Second)
-	require.Nil(t, err)
-	defer replica2.VttabletProcess.TearDown()
-	// We restart replication here because semi-sync will not be set correctly on tablet startup since
-	// we deprecated enable_semi_sync. StartReplication RPC fixes the semi-sync settings by consulting the
-	// durability policies set.
-	err = localCluster.VtctlclientProcess.ExecuteCommand("StopReplication", replica2.Alias)
-	require.NoError(t, err)
-	err = localCluster.VtctlclientProcess.ExecuteCommand("StartReplication", replica2.Alias)
-	require.NoError(t, err)
-
-	// validate that semi-sync is enabled for replica, disable for rdOnly
-	if replica2.Type == "replica" {
-		verifySemiSyncStatus(t, replica2, "ON")
-	} else if replica2.Type == "rdonly" {
-		verifySemiSyncStatus(t, replica2, "OFF")
-	}
-
-	// validate that new replica has all the data
-	cluster.VerifyRowsInTablet(t, replica2, keyspaceName, 2)
-
-	// Remove all backups
-	localCluster.RemoveAllBackups(t, shardKsName)
-
-}
-
-// TestBackupTransformErrorImpl validates backup behavior with transform hook
-// when the hook encounters an error
-func TestBackupTransformErrorImpl(t *testing.T) {
-	// restart the replica with transform hook parameter
-	defer cluster.PanicHandler(t)
-	err := replica1.VttabletProcess.TearDown()
-	require.Nil(t, err)
-
-	replica1.VttabletProcess.ExtraArgs = []string{
-		"--db-credentials-file", dbCredentialFile,
-		"--backup_storage_hook", "test_backup_error",
-		"--restore_from_backup",
-		"--backup_storage_implementation", "file",
-		"--file_backup_storage_root", localCluster.VtctldProcess.FileBackupStorageRoot}
-	replica1.VttabletProcess.ServingStatus = "SERVING"
-	err = replica1.VttabletProcess.Setup()
-	require.Nil(t, err)
-
-	// create backup, it should fail
-	out, err := localCluster.VtctlclientProcess.ExecuteCommandWithOutput("Backup", replica1.Alias)
-	require.NotNil(t, err)
-	require.Containsf(t, out, "backup is not usable, aborting it", "unexpected error received %v", err)
-
-	// validate there is no backup left
-	localCluster.VerifyBackupCount(t, shardKsName, 0)
-}
-
-// validateManifestFile reads manifest and validates that it
-// has a TransformHook, SkipCompress and FileEntries. It also
-// validates that backup_files available in FileEntries have
-// 'header' as their first line.
-func validateManifestFile(t *testing.T, backupLocation string) {
-
-	// reading manifest
-	data, err := os.ReadFile(backupLocation + "/MANIFEST")
-	require.Nilf(t, err, "error while reading MANIFEST %v", err)
-	manifest := make(map[string]any)
-
-	// parsing manifest
-	err = json.Unmarshal(data, &manifest)
-	require.Nilf(t, err, "error while parsing MANIFEST %v", err)
-
-	// validate manifest
-	transformHook := manifest["TransformHook"]
-	require.Equalf(t, "test_backup_transform", transformHook, "invalid transformHook in MANIFEST")
-	skipCompress := manifest["SkipCompress"]
-	assert.Equalf(t, skipCompress, true, "invalid value of skipCompress")
-
-	// validate backup files
-	fielEntries := manifest["FileEntries"]
-	fileArr, ok := fielEntries.([]any)
-	require.True(t, ok)
-	for i := range fileArr {
-		f, err := os.Open(fmt.Sprintf("%s/%d", backupLocation, i))
-		require.Nilf(t, err, "error while opening backup_file %d: %v", i, err)
-		var fileHeader string
-		_, err = fmt.Fscanln(f, &fileHeader)
-		f.Close()
-
-		require.Nilf(t, err, "error while reading backup_file %d: %v", i, err)
-		require.Equalf(t, "header", fileHeader, "wrong file contents for %d", i)
-	}
-
-}
-
-// verifySemiSyncStatus validates the replication status in tablet.
-func verifySemiSyncStatus(t *testing.T, vttablet *cluster.Vttablet, expectedStatus string) {
-	status, err := vttablet.VttabletProcess.GetDBVar("rpl_semi_sync_slave_enabled", keyspaceName)
-	require.Nil(t, err)
-	assert.Equal(t, expectedStatus, status)
-	status, err = vttablet.VttabletProcess.GetDBStatus("rpl_semi_sync_slave_status", keyspaceName)
-	require.Nil(t, err)
-	assert.Equal(t, expectedStatus, status)
-}
-
-// verifyInitialReplication generates a record on the primary and verifies that the record
-// exists on all tablets in the shard. We also check the PRIMARY as with lossless semi-sync
-// it's possible that a GTID is applied on a replica but not (yet) on the source:
-// http://my-replication-life.blogspot.com/2013/09/loss-less-semi-synchronous-replication.html
-func verifyInitialReplication(t *testing.T) {
-	// confirm that semi-sync is enabled for the replica tablets
-	healthyReplicaCount := 0
-	for _, tablet := range testTablets {
-		if tablet.Type == "replica" {
-			verifySemiSyncStatus(t, tablet, "ON")
-			healthyReplicaCount++
-		}
-	}
-
-	if healthyReplicaCount < 2 {
-		log.Errorf("Not enough healthy replicas to guarantee safe backups when semi-sync is enabled! Should have at least two, currently have %d",
-			healthyReplicaCount)
-	}
-
-	_, err := primary.VttabletProcess.QueryTablet(vtInsertTest, keyspaceName, true)
-	require.Nil(t, err)
-	_, err = primary.VttabletProcess.QueryTablet("insert into vt_insert_test (msg) values ('test1')", keyspaceName, true)
-	require.Nil(t, err)
-
-	for _, tablet := range testTablets {
-		cluster.VerifyRowsInTablet(t, tablet, keyspaceName, 1)
-	}
-}
diff --git a/go/test/endtoend/backup/transform/mysqlctld/backup_transform_mysqlctld_test.go b/go/test/endtoend/backup/transform/mysqlctld/backup_transform_mysqlctld_test.go
deleted file mode 100644
index 0a3b11227d..0000000000
--- a/go/test/endtoend/backup/transform/mysqlctld/backup_transform_mysqlctld_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-Copyright 2019 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 mysqlctld
-
-import (
-	"testing"
-
-	"vitess.io/vitess/go/test/endtoend/backup/transform"
-)
-
-func TestMain(m *testing.M) {
-	transform.TestMainSetup(m, true)
-}
-
-func TestBackupTransform(t *testing.T) {
-	transform.TestBackupTransformImpl(t)
-}
-func TestBackupTransformError(t *testing.T) {
-	transform.TestBackupTransformErrorImpl(t)
-}
diff --git a/go/test/endtoend/cluster/mysqlctl_process.go b/go/test/endtoend/cluster/mysqlctl_process.go
index 3478d5dc80..21908efafd 100644
--- a/go/test/endtoend/cluster/mysqlctl_process.go
+++ b/go/test/endtoend/cluster/mysqlctl_process.go
@@ -104,13 +104,22 @@ func (mysqlctl *MysqlctlProcess) startProcess(init bool) (*exec.Cmd, error) {
 		if mysqlctl.SecureTransport {
 			// Set up EXTRA_MY_CNF for ssl
 			sslPath := path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/ssl_%010d", mysqlctl.TabletUID))
+			os.MkdirAll(sslPath, 0755)
+
+			// create certificates
+			clientServerKeyPair := tlstest.CreateClientServerCertPairs(sslPath)
+
+			// use the certificate values in template to create cnf file
 			sslPathData := struct {
-				Dir string
+				Dir        string
+				ServerCert string
+				ServerKey  string
 			}{
-				Dir: sslPath,
+				Dir:        sslPath,
+				ServerCert: clientServerKeyPair.ServerCert,
+				ServerKey:  clientServerKeyPair.ServerKey,
 			}
 
-			os.MkdirAll(sslPath, 0755)
 			extraMyCNF := path.Join(sslPath, "ssl.cnf")
 			fout, err := os.Create(extraMyCNF)
 			if err != nil {
@@ -120,15 +129,13 @@ func (mysqlctl *MysqlctlProcess) startProcess(init bool) (*exec.Cmd, error) {
 
 			template.Must(template.New(fmt.Sprintf("%010d", mysqlctl.TabletUID)).Parse(`
 ssl_ca={{.Dir}}/ca-cert.pem
-ssl_cert={{.Dir}}/server-001-cert.pem
-ssl_key={{.Dir}}/server-001-key.pem
+ssl_cert={{.ServerCert}}
+ssl_key={{.ServerKey}}
 `)).Execute(fout, sslPathData)
 			if err := fout.Close(); err != nil {
 				return nil, err
 			}
 
-			tlstest.CreateClientServerCertPairs(sslPath)
-
 			tmpProcess.Env = append(tmpProcess.Env, "EXTRA_MY_CNF="+extraMyCNF)
 			tmpProcess.Env = append(tmpProcess.Env, "VTDATAROOT="+os.Getenv("VTDATAROOT"))
 		}
diff --git a/go/test/endtoend/onlineddl/vrepl_stress/onlineddl_vrepl_mini_stress_test.go b/go/test/endtoend/onlineddl/vrepl_stress/onlineddl_vrepl_mini_stress_test.go
index 0531af319b..57e7029f56 100644
--- a/go/test/endtoend/onlineddl/vrepl_stress/onlineddl_vrepl_mini_stress_test.go
+++ b/go/test/endtoend/onlineddl/vrepl_stress/onlineddl_vrepl_mini_stress_test.go
@@ -140,6 +140,7 @@ const (
 	maxConcurrency                = 20
 	singleConnectionSleepInterval = 2 * time.Millisecond
 	countIterations               = 5
+	migrationWaitTimeout          = 60 * time.Second
 )
 
 func resetOpOrder() {
@@ -344,7 +345,7 @@ func testOnlineDDLStatement(t *testing.T, alterStatement string, ddlStrategy str
 	assert.NoError(t, err)
 
 	if !strategySetting.Strategy.IsDirect() {
-		status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, 30*time.Second, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
+		status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, migrationWaitTimeout, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
 		fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
 	}
 
diff --git a/go/test/endtoend/recovery/pitr/shardedpitr_test.go b/go/test/endtoend/recovery/pitr/shardedpitr_test.go
index 6ae2e72b1a..a594fe05d0 100644
--- a/go/test/endtoend/recovery/pitr/shardedpitr_test.go
+++ b/go/test/endtoend/recovery/pitr/shardedpitr_test.go
@@ -40,12 +40,15 @@ var (
 var (
 	clusterInstance *cluster.LocalProcessCluster
 
-	primary       *cluster.Vttablet
-	replica       *cluster.Vttablet
-	shard0Primary *cluster.Vttablet
-	shard0Replica *cluster.Vttablet
-	shard1Primary *cluster.Vttablet
-	shard1Replica *cluster.Vttablet
+	primary        *cluster.Vttablet
+	replica1       *cluster.Vttablet
+	replica2       *cluster.Vttablet
+	shard0Primary  *cluster.Vttablet
+	shard0Replica1 *cluster.Vttablet
+	shard0Replica2 *cluster.Vttablet
+	shard1Primary  *cluster.Vttablet
+	shard1Replica1 *cluster.Vttablet
+	shard1Replica2 *cluster.Vttablet
 
 	cell           = "zone1"
 	hostname       = "localhost"
@@ -127,10 +130,10 @@ func TestPITRRecovery(t *testing.T) {
 	insertRow(t, 1, "prd-1", false)
 	insertRow(t, 2, "prd-2", false)
 
-	cluster.VerifyRowsInTabletForTable(t, replica, keyspaceName, 2, "product")
+	cluster.VerifyRowsInTabletForTable(t, replica1, keyspaceName, 2, "product")
 
 	// backup the replica
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", replica.Alias)
+	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", replica1.Alias)
 	require.NoError(t, err)
 
 	// check that the backup shows up in the listing
@@ -166,14 +169,14 @@ func TestPITRRecovery(t *testing.T) {
 	}
 
 	// wait till all the shards have required data
-	cluster.VerifyRowsInTabletForTable(t, shard0Replica, keyspaceName, 6, "product")
-	cluster.VerifyRowsInTabletForTable(t, shard1Replica, keyspaceName, 4, "product")
+	cluster.VerifyRowsInTabletForTable(t, shard0Replica1, keyspaceName, 6, "product")
+	cluster.VerifyRowsInTabletForTable(t, shard1Replica1, keyspaceName, 4, "product")
 
 	// take the backup (to simulate the regular backup)
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", shard0Replica.Alias)
+	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", shard0Replica1.Alias)
 	require.NoError(t, err)
 	// take the backup (to simulate the regular backup)
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", shard1Replica.Alias)
+	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", shard1Replica1.Alias)
 	require.NoError(t, err)
 
 	backups, err := clusterInstance.ListBackups(keyspaceName + "/-80")
@@ -313,9 +316,9 @@ func performResharding(t *testing.T) {
 	require.NoError(t, err)
 
 	// remove the original tablets in the original shard
-	removeTablets(t, []*cluster.Vttablet{primary, replica})
+	removeTablets(t, []*cluster.Vttablet{primary, replica1, replica2})
 
-	for _, tablet := range []*cluster.Vttablet{replica} {
+	for _, tablet := range []*cluster.Vttablet{replica1, replica2} {
 		err = clusterInstance.VtctlclientProcess.ExecuteCommand("DeleteTablet", tablet.Alias)
 		require.NoError(t, err)
 	}
@@ -394,15 +397,18 @@ func initializeCluster(t *testing.T) {
 
 	// Defining all the tablets
 	primary = clusterInstance.NewVttabletInstance("replica", 0, "")
-	replica = clusterInstance.NewVttabletInstance("replica", 0, "")
+	replica1 = clusterInstance.NewVttabletInstance("replica", 0, "")
+	replica2 = clusterInstance.NewVttabletInstance("replica", 0, "")
 	shard0Primary = clusterInstance.NewVttabletInstance("replica", 0, "")
-	shard0Replica = clusterInstance.NewVttabletInstance("replica", 0, "")
+	shard0Replica1 = clusterInstance.NewVttabletInstance("replica", 0, "")
+	shard0Replica2 = clusterInstance.NewVttabletInstance("replica", 0, "")
 	shard1Primary = clusterInstance.NewVttabletInstance("replica", 0, "")
-	shard1Replica = clusterInstance.NewVttabletInstance("replica", 0, "")
+	shard1Replica1 = clusterInstance.NewVttabletInstance("replica", 0, "")
+	shard1Replica2 = clusterInstance.NewVttabletInstance("replica", 0, "")
 
-	shard.Vttablets = []*cluster.Vttablet{primary, replica}
-	shard0.Vttablets = []*cluster.Vttablet{shard0Primary, shard0Replica}
-	shard1.Vttablets = []*cluster.Vttablet{shard1Primary, shard1Replica}
+	shard.Vttablets = []*cluster.Vttablet{primary, replica1, replica2}
+	shard0.Vttablets = []*cluster.Vttablet{shard0Primary, shard0Replica1, shard0Replica2}
+	shard1.Vttablets = []*cluster.Vttablet{shard1Primary, shard1Replica1, shard1Replica2}
 
 	clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, commonTabletArg...)
 	clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, "--restore_from_backup")
@@ -436,14 +442,16 @@ func initializeCluster(t *testing.T) {
 		"FLUSH PRIVILEGES;",
 	}
 
-	for _, tablet := range []*cluster.Vttablet{primary, replica, shard0Primary, shard0Replica, shard1Primary, shard1Replica} {
-		for _, query := range queryCmds {
-			_, err = tablet.VttabletProcess.QueryTablet(query, keyspace.Name, false)
+	for _, shard := range clusterInstance.Keyspaces[0].Shards {
+		for _, tablet := range shard.Vttablets {
+			for _, query := range queryCmds {
+				_, err = tablet.VttabletProcess.QueryTablet(query, keyspace.Name, false)
+				require.NoError(t, err)
+			}
+
+			err = tablet.VttabletProcess.Setup()
 			require.NoError(t, err)
 		}
-
-		err = tablet.VttabletProcess.Setup()
-		require.NoError(t, err)
 	}
 
 	err = clusterInstance.VtctlclientProcess.InitShardPrimary(keyspaceName, shard.Name, cell, primary.TabletUID)
diff --git a/go/test/endtoend/recovery/pitrtls/shardedpitr_tls_test.go b/go/test/endtoend/recovery/pitrtls/shardedpitr_tls_test.go
deleted file mode 100644
index ab0020d373..0000000000
--- a/go/test/endtoend/recovery/pitrtls/shardedpitr_tls_test.go
+++ /dev/null
@@ -1,544 +0,0 @@
-/*
-Copyright 2020 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 pitrtls
-
-import (
-	"context"
-	"crypto/x509"
-	"encoding/pem"
-	"fmt"
-	"os"
-	"os/exec"
-	"path"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"vitess.io/vitess/go/mysql"
-	"vitess.io/vitess/go/test/endtoend/cluster"
-	"vitess.io/vitess/go/vt/log"
-)
-
-var (
-	createTable = `create table product (id bigint(20) primary key, name char(10), created bigint(20));`
-	insertTable = `insert into product (id, name, created) values(%d, '%s', unix_timestamp());`
-	getCountID  = `select count(*) from product`
-)
-
-var (
-	clusterInstance *cluster.LocalProcessCluster
-
-	primary       *cluster.Vttablet
-	replica       *cluster.Vttablet
-	shard0Primary *cluster.Vttablet
-	shard0Replica *cluster.Vttablet
-	shard1Primary *cluster.Vttablet
-	shard1Replica *cluster.Vttablet
-
-	cell           = "zone1"
-	hostname       = "localhost"
-	keyspaceName   = "ks"
-	restoreKS1Name = "restoreks1"
-	restoreKS2Name = "restoreks2"
-	restoreKS3Name = "restoreks3"
-	shardName      = "0"
-	shard0Name     = "-80"
-	shard1Name     = "80-"
-	dbName         = "vt_ks"
-	mysqlUserName  = "vt_dba"
-	mysqlPassword  = "password"
-	vSchema        = `{
-		"sharded": true,
-		"vindexes": {
-			"hash_index": {
-				"type": "hash"
-			}
-		},
-		"tables": {
-			"product": {
-				"column_vindexes": [
-					{
-						"column": "id",
-						"name": "hash_index"
-					}
-				]
-			}
-		}
-	}`
-	commonTabletArg = []string{
-		"--vreplication_healthcheck_topology_refresh", "1s",
-		"--vreplication_healthcheck_retry_delay", "1s",
-		"--vreplication_retry_delay", "1s",
-		"--degraded_threshold", "5s",
-		"--lock_tables_timeout", "5s",
-		"--watch_replication_stream",
-		"--serving_state_grace_period", "1s"}
-)
-
-func removeTablets(t *testing.T, tablets []*cluster.Vttablet) {
-	var mysqlProcs []*exec.Cmd
-	for _, tablet := range tablets {
-		proc, _ := tablet.MysqlctlProcess.StopProcess()
-		mysqlProcs = append(mysqlProcs, proc)
-	}
-	for _, proc := range mysqlProcs {
-		err := proc.Wait()
-		require.NoError(t, err)
-	}
-	for _, tablet := range tablets {
-		tablet.VttabletProcess.TearDown()
-	}
-}
-
-func initializeCluster(t *testing.T) {
-	clusterInstance = cluster.NewCluster(cell, hostname)
-
-	// Start topo server
-	err := clusterInstance.StartTopo()
-	require.NoError(t, err)
-
-	// Start keyspace
-	keyspace := &cluster.Keyspace{
-		Name: keyspaceName,
-	}
-	clusterInstance.Keyspaces = append(clusterInstance.Keyspaces, *keyspace)
-
-	shard := &cluster.Shard{
-		Name: shardName,
-	}
-	shard0 := &cluster.Shard{
-		Name: shard0Name,
-	}
-	shard1 := &cluster.Shard{
-		Name: shard1Name,
-	}
-
-	// Defining all the tablets
-	primary = clusterInstance.NewVttabletInstance("replica", 0, "")
-	replica = clusterInstance.NewVttabletInstance("replica", 0, "")
-	shard0Primary = clusterInstance.NewVttabletInstance("replica", 0, "")
-	shard0Replica = clusterInstance.NewVttabletInstance("replica", 0, "")
-	shard1Primary = clusterInstance.NewVttabletInstance("replica", 0, "")
-	shard1Replica = clusterInstance.NewVttabletInstance("replica", 0, "")
-
-	shard.Vttablets = []*cluster.Vttablet{primary, replica}
-	shard0.Vttablets = []*cluster.Vttablet{shard0Primary, shard0Replica}
-	shard1.Vttablets = []*cluster.Vttablet{shard1Primary, shard1Replica}
-
-	clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, commonTabletArg...)
-	clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, "--restore_from_backup")
-
-	err = clusterInstance.SetupCluster(keyspace, []cluster.Shard{*shard, *shard0, *shard1})
-	require.NoError(t, err)
-	vtctldClientProcess := cluster.VtctldClientProcessInstance("localhost", clusterInstance.VtctldProcess.GrpcPort, clusterInstance.TmpDirectory)
-	out, err := vtctldClientProcess.ExecuteCommandWithOutput("SetKeyspaceDurabilityPolicy", keyspaceName, "--durability-policy=semi_sync")
-	require.NoError(t, err, out)
-	// Start MySql
-	var mysqlCtlProcessList []*exec.Cmd
-	for _, shard := range clusterInstance.Keyspaces[0].Shards {
-		for _, tablet := range shard.Vttablets {
-			tablet.MysqlctlProcess.SecureTransport = true
-			proc, err := tablet.MysqlctlProcess.StartProcess()
-			require.NoError(t, err)
-			mysqlCtlProcessList = append(mysqlCtlProcessList, proc)
-		}
-	}
-
-	// Wait for mysql processes to start
-	for _, proc := range mysqlCtlProcessList {
-		err = proc.Wait()
-		require.NoError(t, err)
-	}
-
-	queryCmds := []string{
-		fmt.Sprintf("CREATE USER '%s'@'%%' IDENTIFIED BY '%s';", mysqlUserName, mysqlPassword),
-		fmt.Sprintf("GRANT ALL ON *.* TO '%s'@'%%';", mysqlUserName),
-		fmt.Sprintf("GRANT GRANT OPTION ON *.* TO '%s'@'%%';", mysqlUserName),
-		fmt.Sprintf("create database %s;", "vt_ks"),
-		"FLUSH PRIVILEGES;",
-	}
-
-	for _, tablet := range []*cluster.Vttablet{primary, replica, shard0Primary, shard0Replica, shard1Primary, shard1Replica} {
-		for _, query := range queryCmds {
-			_, err = tablet.VttabletProcess.QueryTablet(query, keyspace.Name, false)
-			require.NoError(t, err)
-		}
-
-		err = tablet.VttabletProcess.Setup()
-		require.NoError(t, err)
-	}
-
-	err = clusterInstance.VtctlclientProcess.InitShardPrimary(keyspaceName, shard.Name, cell, primary.TabletUID)
-	require.NoError(t, err)
-
-	// Start vtgate
-	err = clusterInstance.StartVtgate()
-	require.NoError(t, err)
-}
-
-func insertRow(t *testing.T, id int, productName string, isSlow bool) {
-	ctx := context.Background()
-	vtParams := mysql.ConnParams{
-		Host: clusterInstance.Hostname,
-		Port: clusterInstance.VtgateMySQLPort,
-	}
-	conn, err := mysql.Connect(ctx, &vtParams)
-	require.NoError(t, err)
-	defer conn.Close()
-
-	insertSmt := fmt.Sprintf(insertTable, id, productName)
-	_, err = conn.ExecuteFetch(insertSmt, 1000, true)
-	require.NoError(t, err)
-
-	if isSlow {
-		time.Sleep(1 * time.Second)
-	}
-}
-
-func createRestoreKeyspace(t *testing.T, timeToRecover, restoreKeyspaceName string) {
-	output, err := clusterInstance.VtctlclientProcess.ExecuteCommandWithOutput("CreateKeyspace", "--",
-		"--keyspace_type=SNAPSHOT", "--base_keyspace="+keyspaceName,
-		"--snapshot_time", timeToRecover, restoreKeyspaceName)
-	log.Info(output)
-	require.NoError(t, err)
-}
-
-// Test pitr (Point in time recovery).
-// -------------------------------------------
-// The following test will:
-// - create a shard with primary and replica
-// - run InitShardPrimary
-// - insert some data using vtgate (e.g. here we have inserted rows 1,2)
-// - verify the replication
-// - take backup of replica
-// - insert some data using vtgate (e.g. we inserted rows 3 4 5 6), while inserting row-4, note down the time (restoreTime1)
-// - perform a resharding to create 2 shards (-80, 80-), and delete the old shard
-// - insert some data using vtgate (e.g. we will insert 7 8 9 10) and verify we get required number of rows in -80, 80- shard
-// - take backup of both shards
-// - insert some more data using vtgate (e.g. we will insert 11 12 13 14 15), while inserting row-13, note down the time (restoreTime2)
-// - note down the current time (restoreTime3)
-
-// - Till now we did all the presetup for assertions
-
-// - asserting that restoring to restoreTime1 (going from 2 shards to 1 shard) is working, i.e. we should get 4 rows.
-// - asserting that while restoring if we give small timeout value, it will restore upto to the last available backup (asserting only -80 shard)
-// - asserting that restoring to restoreTime2 (going from 2 shards to 2 shards with past time) is working, it will assert for both shards
-// - asserting that restoring to restoreTime3 is working, we should get complete data after restoring,  as we have in existing shards.
-func TestTLSPITRRecovery(t *testing.T) {
-	defer cluster.PanicHandler(nil)
-	initializeCluster(t)
-	defer clusterInstance.Teardown()
-
-	// Creating the table
-	_, err := primary.VttabletProcess.QueryTablet(createTable, keyspaceName, true)
-	require.NoError(t, err)
-
-	insertRow(t, 1, "prd-1", false)
-	insertRow(t, 2, "prd-2", false)
-
-	cluster.VerifyRowsInTabletForTable(t, replica, keyspaceName, 2, "product")
-
-	// backup the replica
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", replica.Alias)
-	require.NoError(t, err)
-
-	// check that the backup shows up in the listing
-	output, err := clusterInstance.ListBackups("ks/0")
-	require.NoError(t, err)
-	assert.Equal(t, 1, len(output))
-
-	// now insert some more data to simulate the changes after regular backup
-	// every insert has some time lag/difference to simulate the time gap between rows
-	// and when we recover to certain time, this time gap will be able to identify the exact eligible row
-	var restoreTime1 string
-	for counter := 3; counter <= 6; counter++ {
-		if counter == 4 { // we want to recovery till this, so noting the time
-			tm := time.Now().Add(1 * time.Second).UTC()
-			restoreTime1 = tm.Format(time.RFC3339)
-		}
-		insertRow(t, counter, fmt.Sprintf("prd-%d", counter), true)
-	}
-
-	// creating restore keyspace with snapshot time as restoreTime1
-	//   Need to test this before resharding and we tear down the
-	//   original mysql replica, which we use as a binlog source
-	createRestoreKeyspace(t, restoreTime1, restoreKS1Name)
-
-	// Launching a recovery tablet which recovers data from the primary till the restoreTime1
-	tlsTestTabletRecovery(t, replica, "2m", restoreKS1Name, "0", "INT64(4)")
-
-	// starting resharding process
-	tlsPerformResharding(t)
-
-	for counter := 7; counter <= 10; counter++ {
-		insertRow(t, counter, fmt.Sprintf("prd-%d", counter), false)
-	}
-
-	// wait till all the shards have required data
-	cluster.VerifyRowsInTabletForTable(t, shard0Replica, keyspaceName, 6, "product")
-	cluster.VerifyRowsInTabletForTable(t, shard1Replica, keyspaceName, 4, "product")
-
-	// take the backup (to simulate the regular backup)
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", shard0Replica.Alias)
-	require.NoError(t, err)
-	// take the backup (to simulate the regular backup)
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Backup", shard1Replica.Alias)
-	require.NoError(t, err)
-
-	backups, err := clusterInstance.ListBackups(keyspaceName + "/-80")
-	require.NoError(t, err)
-	require.Equal(t, len(backups), 1)
-
-	backups, err = clusterInstance.ListBackups(keyspaceName + "/80-")
-	require.NoError(t, err)
-	require.Equal(t, len(backups), 1)
-
-	// now insert some more data to simulate the changes after regular backup
-	// every insert has some time lag/difference to simulate the time gap between rows
-	// and when we recover to certain time, this time gap will be able to identify the exact eligible row
-	var restoreTime2 string
-	for counter := 11; counter <= 15; counter++ {
-		if counter == 13 { // we want to recovery till this, so noting the time
-			tm := time.Now().Add(1 * time.Second).UTC()
-			restoreTime2 = tm.Format(time.RFC3339)
-		}
-		insertRow(t, counter, fmt.Sprintf("prd-%d", counter), true)
-	}
-	restoreTime3 := time.Now().UTC().Format(time.RFC3339)
-
-	// create restoreKeyspace with snapshot time as restoreTime2
-	createRestoreKeyspace(t, restoreTime2, restoreKS2Name)
-
-	// test the recovery with smaller binlog_lookup_timeout for shard0
-	// since we have small lookup timeout, it will just get whatever available in the backup
-	// mysql> select * from product;
-	// +----+--------+------------+
-	// | id | name   | created    |
-	// +----+--------+------------+
-	// |  1 | prd-1  | 1597219030 |
-	// |  2 | prd-2  | 1597219030 |
-	// |  3 | prd-3  | 1597219043 |
-	// |  5 | prd-5  | 1597219045 |
-	// |  9 | prd-9  | 1597219130 |
-	// | 10 | prd-10 | 1597219130 |
-	// +----+--------+------------+
-	tlsTestTabletRecovery(t, shard0Replica, "1ms", restoreKS2Name, "-80", "INT64(6)")
-
-	// test the recovery with valid binlog_lookup_timeout for shard0 and getting the data till the restoreTime2
-	// 	mysql> select * from product;
-	// +----+--------+------------+
-	// | id | name   | created    |
-	// +----+--------+------------+
-	// |  1 | prd-1  | 1597219030 |
-	// |  2 | prd-2  | 1597219030 |
-	// |  3 | prd-3  | 1597219043 |
-	// |  5 | prd-5  | 1597219045 |
-	// |  9 | prd-9  | 1597219130 |
-	// | 10 | prd-10 | 1597219130 |
-	// | 13 | prd-13 | 1597219141 |
-	// +----+--------+------------+
-	tlsTestTabletRecovery(t, shard0Replica, "2m", restoreKS2Name, "-80", "INT64(7)")
-
-	// test the recovery with valid binlog_lookup_timeout for shard1 and getting the data till the restoreTime2
-	// 	mysql> select * from product;
-	// +----+--------+------------+
-	// | id | name   | created    |
-	// +----+--------+------------+
-	// |  4 | prd-4  | 1597219044 |
-	// |  6 | prd-6  | 1597219046 |
-	// |  7 | prd-7  | 1597219130 |
-	// |  8 | prd-8  | 1597219130 |
-	// | 11 | prd-11 | 1597219139 |
-	// | 12 | prd-12 | 1597219140 |
-	// +----+--------+------------+
-	tlsTestTabletRecovery(t, shard1Replica, "2m", restoreKS2Name, "80-", "INT64(6)")
-
-	// test the recovery with timetorecover > (timestamp of last binlog event in binlog server)
-	createRestoreKeyspace(t, restoreTime3, restoreKS3Name)
-
-	// 	mysql> select * from product;
-	// +----+--------+------------+
-	// | id | name   | created    |
-	// +----+--------+------------+
-	// |  1 | prd-1  | 1597219030 |
-	// |  2 | prd-2  | 1597219030 |
-	// |  3 | prd-3  | 1597219043 |
-	// |  5 | prd-5  | 1597219045 |
-	// |  9 | prd-9  | 1597219130 |
-	// | 10 | prd-10 | 1597219130 |
-	// | 13 | prd-13 | 1597219141 |
-	// | 15 | prd-15 | 1597219142 |
-	// +----+--------+------------+
-	tlsTestTabletRecovery(t, shard0Replica, "2m", restoreKS3Name, "-80", "INT64(8)")
-
-	// 	mysql> select * from product;
-	// +----+--------+------------+
-	// | id | name   | created    |
-	// +----+--------+------------+
-	// |  4 | prd-4  | 1597219044 |
-	// |  6 | prd-6  | 1597219046 |
-	// |  7 | prd-7  | 1597219130 |
-	// |  8 | prd-8  | 1597219130 |
-	// | 11 | prd-11 | 1597219139 |
-	// | 12 | prd-12 | 1597219140 |
-	// | 14 | prd-14 | 1597219142 |
-	// +----+--------+------------+
-	tlsTestTabletRecovery(t, shard1Replica, "2m", restoreKS3Name, "80-", "INT64(7)")
-}
-
-func tlsPerformResharding(t *testing.T) {
-	err := clusterInstance.VtctlclientProcess.ApplyVSchema(keyspaceName, vSchema)
-	require.NoError(t, err)
-
-	err = clusterInstance.VtctlProcess.ExecuteCommand("InitShardPrimary", "--", "--force", "ks/-80", shard0Primary.Alias)
-	require.NoError(t, err)
-
-	err = clusterInstance.VtctlProcess.ExecuteCommand("InitShardPrimary", "--", "--force", "ks/80-", shard1Primary.Alias)
-	require.NoError(t, err)
-
-	// we need to create the schema, and the worker will do data copying
-	for _, keyspaceShard := range []string{"ks/-80", "ks/80-"} {
-		err = clusterInstance.VtctlclientProcess.ExecuteCommand("CopySchemaShard", "ks/0", keyspaceShard)
-		require.NoError(t, err)
-	}
-
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("Reshard", "ks.reshardWorkflow", "0", "--", "-80,80-")
-	require.NoError(t, err)
-
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("SwitchReads", "--", "--tablet_type=rdonly", "ks.reshardWorkflow")
-	require.NoError(t, err)
-
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("SwitchReads", "--", "--tablet_type=replica", "ks.reshardWorkflow")
-	require.NoError(t, err)
-
-	// then serve primary from the split shards
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("SwitchWrites", "ks.reshardWorkflow")
-	require.NoError(t, err)
-
-	// remove the original tablets in the original shard
-	removeTablets(t, []*cluster.Vttablet{primary, replica})
-
-	for _, tablet := range []*cluster.Vttablet{replica} {
-		err = clusterInstance.VtctlclientProcess.ExecuteCommand("DeleteTablet", tablet.Alias)
-		require.NoError(t, err)
-	}
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("DeleteTablet", "--", "--allow_primary", primary.Alias)
-	require.NoError(t, err)
-
-	// rebuild the serving graph, all mentions of the old shards should be gone
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("RebuildKeyspaceGraph", "ks")
-	require.NoError(t, err)
-
-	// delete the original shard
-	err = clusterInstance.VtctlclientProcess.ExecuteCommand("DeleteShard", "ks/0")
-	require.NoError(t, err)
-
-	// Restart vtgate process
-	err = clusterInstance.VtgateProcess.TearDown()
-	require.NoError(t, err)
-
-	err = clusterInstance.VtgateProcess.Setup()
-	require.NoError(t, err)
-
-	clusterInstance.WaitForTabletsToHealthyInVtgate()
-}
-
-func tlsTestTabletRecovery(t *testing.T, tabletForBinlogs *cluster.Vttablet, lookupTimeout, restoreKeyspaceName, shardName, expectedRows string) {
-	recoveryTablet := clusterInstance.NewVttabletInstance("replica", 0, cell)
-	tlsLaunchRecoveryTablet(t, recoveryTablet, tabletForBinlogs, lookupTimeout, restoreKeyspaceName, shardName)
-
-	sqlRes, err := recoveryTablet.VttabletProcess.QueryTablet(getCountID, keyspaceName, true)
-	require.NoError(t, err)
-	assert.Equal(t, expectedRows, sqlRes.Rows[0][0].String())
-
-	defer recoveryTablet.MysqlctlProcess.Stop()
-	defer recoveryTablet.VttabletProcess.TearDown()
-}
-
-func tlsLaunchRecoveryTablet(t *testing.T, tablet *cluster.Vttablet, tabletForBinlogs *cluster.Vttablet, lookupTimeout, restoreKeyspaceName, shardName string) {
-	tablet.MysqlctlProcess = *cluster.MysqlCtlProcessInstance(tablet.TabletUID, tablet.MySQLPort, clusterInstance.TmpDirectory)
-	tablet.MysqlctlProcess.SecureTransport = true
-	err := tablet.MysqlctlProcess.Start()
-	require.NoError(t, err)
-
-	tablet.VttabletProcess = cluster.VttabletProcessInstance(
-		tablet.HTTPPort,
-		tablet.GrpcPort,
-		tablet.TabletUID,
-		clusterInstance.Cell,
-		shardName,
-		keyspaceName,
-		clusterInstance.VtctldProcess.Port,
-		tablet.Type,
-		clusterInstance.TopoProcess.Port,
-		clusterInstance.Hostname,
-		clusterInstance.TmpDirectory,
-		clusterInstance.VtTabletExtraArgs,
-		clusterInstance.EnableSemiSync,
-		clusterInstance.DefaultCharset)
-	tablet.Alias = tablet.VttabletProcess.TabletPath
-	tablet.VttabletProcess.SupportsBackup = true
-	tablet.VttabletProcess.Keyspace = restoreKeyspaceName
-
-	certDir := path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/ssl_%010d", tablet.MysqlctlProcess.TabletUID))
-	tablet.VttabletProcess.ExtraArgs = []string{
-		"--disable_active_reparents",
-		"--enable_replication_reporter=false",
-		"--init_db_name_override", dbName,
-		"--init_tablet_type", "replica",
-		"--init_keyspace", restoreKeyspaceName,
-		"--init_shard", shardName,
-		"--binlog_host", clusterInstance.Hostname,
-		"--binlog_port", fmt.Sprintf("%d", tabletForBinlogs.MySQLPort),
-		"--binlog_user", mysqlUserName,
-		"--binlog_password", mysqlPassword,
-		"--binlog_ssl_ca", certDir + "/ca-cert.pem",
-		"--binlog_ssl_server_name", getCNFromCertPEM(certDir + "/server-001-cert.pem"),
-		"--pitr_gtid_lookup_timeout", lookupTimeout,
-		"--vreplication_healthcheck_topology_refresh", "1s",
-		"--vreplication_healthcheck_retry_delay", "1s",
-		"--vreplication_tablet_type", "replica",
-		"--vreplication_retry_delay", "1s",
-		"--degraded_threshold", "5s",
-		"--lock_tables_timeout", "5s",
-		"--watch_replication_stream",
-		"--serving_state_grace_period", "1s",
-	}
-	tablet.VttabletProcess.ServingStatus = ""
-
-	err = tablet.VttabletProcess.Setup()
-	require.NoError(t, err)
-
-	tablet.VttabletProcess.WaitForTabletStatusesForTimeout([]string{"SERVING"}, 20*time.Second)
-}
-
-func getCNFromCertPEM(filename string) string {
-	pemBytes, _ := os.ReadFile(filename)
-	block, _ := pem.Decode(pemBytes)
-	cert, _ := x509.ParseCertificate(block.Bytes)
-	rdn := cert.Subject.ToRDNSequence()[0][0]
-	t := rdn.Type
-
-	// 2.5.4.3 is ASN OID for "CN"
-	if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 && t[3] == 3 {
-		return fmt.Sprintf("%s", rdn.Value)
-	}
-	// As good a fallback as any
-	return "localhost"
-}
diff --git a/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go b/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
index 820e6ad36e..ba9bbd8b19 100644
--- a/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
+++ b/go/test/endtoend/vtgate/queries/aggregation/aggregation_test.go
@@ -374,3 +374,13 @@ func TestOrderByCount(t *testing.T) {
 
 	mcmp.AssertMatches("SELECT /*vt+ PLANNER=gen4 */ t9.id2 FROM t9 GROUP BY t9.id2 ORDER BY COUNT(t9.id2) DESC", `[[VARCHAR("3")] [VARCHAR("2")] [VARCHAR("1")]]`)
 }
+
+func TestAggregateRandom(t *testing.T) {
+	mcmp, closer := start(t)
+	defer closer()
+
+	mcmp.Exec("insert into t1(t1_id, name, value, shardKey) values (1, 'name 1', 'value 1', 1), (2, 'name 2', 'value 2', 2)")
+	mcmp.Exec("insert into t2(id, shardKey) values (1, 10), (2, 20)")
+
+	mcmp.AssertMatches("SELECT /*vt+ PLANNER=gen4 */ t1.shardKey, t1.name, count(t2.id) FROM t1 JOIN t2 ON t1.value != t2.shardKey GROUP BY t1.t1_id", `[[INT64(1) VARCHAR("name 1") INT64(2)] [INT64(2) VARCHAR("name 2") INT64(2)]]`)
+}
diff --git a/go/vt/schemadiff/table_test.go b/go/vt/schemadiff/table_test.go
index 42f5517fd5..191ee55508 100644
--- a/go/vt/schemadiff/table_test.go
+++ b/go/vt/schemadiff/table_test.go
@@ -988,6 +988,13 @@ func TestCreateTableDiff(t *testing.T) {
 			diff:  "alter table t1 collate utf8mb3_bin",
 			cdiff: "ALTER TABLE `t1` COLLATE utf8mb3_bin",
 		},
+		{
+			name:  "remove table comment",
+			from:  "create table t1 (id int primary key) comment='foo'",
+			to:    "create table t1 (id int primary key)",
+			diff:  "alter table t1 comment ''",
+			cdiff: "ALTER TABLE `t1` COMMENT ''",
+		},
 	}
 	standardHints := DiffHints{}
 	for _, ts := range tt {
diff --git a/go/vt/sqlparser/normalizer_test.go b/go/vt/sqlparser/normalizer_test.go
index f8f72ceff2..3fbb7687d6 100644
--- a/go/vt/sqlparser/normalizer_test.go
+++ b/go/vt/sqlparser/normalizer_test.go
@@ -311,6 +311,13 @@ func TestNormalize(t *testing.T) {
 			"bv1":                   sqltypes.Int64BindVariable(1),
 			"comms_by_companies_id": sqltypes.StringBindVariable("rjve634shXzaavKHbAH16ql6OrxJ"),
 		},
+	}, {
+		// Int leading with zero should also be normalized
+		in:      `select * from t where zipcode = 01001900`,
+		outstmt: `select * from t where zipcode = :zipcode`,
+		outbv: map[string]*querypb.BindVariable{
+			"zipcode": sqltypes.ValueBindVariable(sqltypes.MakeTrusted(sqltypes.Int64, []byte("01001900"))),
+		},
 	}}
 	for _, tc := range testcases {
 		t.Run(tc.in, func(t *testing.T) {
diff --git a/go/vt/vtadmin/api_authz_test.go b/go/vt/vtadmin/api_authz_test.go
index 36edfee181..45d3e443c6 100644
--- a/go/vt/vtadmin/api_authz_test.go
+++ b/go/vt/vtadmin/api_authz_test.go
@@ -3209,7 +3209,8 @@ func testClusters(t testing.TB) []*cluster.Cluster {
 									Keyspace: "test",
 									Name:     "-",
 									Shard: &topodatapb.Shard{
-										KeyRange: &topodatapb.KeyRange{},
+										KeyRange:         &topodatapb.KeyRange{},
+										IsPrimaryServing: true,
 									},
 								},
 							},
diff --git a/go/vt/vtadmin/testutil/authztestgen/config.json b/go/vt/vtadmin/testutil/authztestgen/config.json
index 88470fe7e8..ac89d7f555 100644
--- a/go/vt/vtadmin/testutil/authztestgen/config.json
+++ b/go/vt/vtadmin/testutil/authztestgen/config.json
@@ -23,7 +23,7 @@
                 {
                     "field": "FindAllShardsInKeyspaceResults",
                     "type": "map[string]struct{\nResponse *vtctldatapb.FindAllShardsInKeyspaceResponse\nError error}",
-                    "value": "\"test\": {\nResponse: &vtctldatapb.FindAllShardsInKeyspaceResponse{\nShards: map[string]*vtctldatapb.Shard{\n\"-\": {\nKeyspace: \"test\",\nName: \"-\",\nShard: &topodatapb.Shard{\nKeyRange: &topodatapb.KeyRange{},\n},\n},\n},\n},\n},"
+                    "value": "\"test\": {\nResponse: &vtctldatapb.FindAllShardsInKeyspaceResponse{\nShards: map[string]*vtctldatapb.Shard{\n\"-\": {\nKeyspace: \"test\",\nName: \"-\",\nShard: &topodatapb.Shard{\nKeyRange: &topodatapb.KeyRange{},\nIsPrimaryServing: true,\n},\n},\n},\n},\n},"
                 },
                 {
                     "field": "GetBackupsResults",
diff --git a/go/vt/vtexplain/vtexplain_test.go b/go/vt/vtexplain/vtexplain_test.go
index 21fc30cbd4..8145c59b44 100644
--- a/go/vt/vtexplain/vtexplain_test.go
+++ b/go/vt/vtexplain/vtexplain_test.go
@@ -283,14 +283,14 @@ func TestJSONOutput(t *testing.T) {
 	}
 }
 
-func testShardInfo(ks, start, end string, t *testing.T) *topo.ShardInfo {
+func testShardInfo(ks, start, end string, primaryServing bool, t *testing.T) *topo.ShardInfo {
 	kr, err := key.ParseKeyRangeParts(start, end)
 	require.NoError(t, err)
 
 	return topo.NewShardInfo(
 		ks,
 		fmt.Sprintf("%s-%s", start, end),
-		&topodata.Shard{KeyRange: kr},
+		&topodata.Shard{KeyRange: kr, IsPrimaryServing: primaryServing},
 		&vtexplainTestTopoVersion{},
 	)
 }
@@ -304,14 +304,17 @@ func TestUsingKeyspaceShardMap(t *testing.T) {
 			testcase: "select-sharded-8",
 			ShardRangeMap: map[string]map[string]*topo.ShardInfo{
 				"ks_sharded": {
-					"-20":   testShardInfo("ks_sharded", "", "20", t),
-					"20-40": testShardInfo("ks_sharded", "20", "40", t),
-					"40-60": testShardInfo("ks_sharded", "40", "60", t),
-					"60-80": testShardInfo("ks_sharded", "60", "80", t),
-					"80-a0": testShardInfo("ks_sharded", "80", "a0", t),
-					"a0-c0": testShardInfo("ks_sharded", "a0", "c0", t),
-					"c0-e0": testShardInfo("ks_sharded", "c0", "e0", t),
-					"e0-":   testShardInfo("ks_sharded", "e0", "", t),
+					"-20":   testShardInfo("ks_sharded", "", "20", true, t),
+					"20-40": testShardInfo("ks_sharded", "20", "40", true, t),
+					"40-60": testShardInfo("ks_sharded", "40", "60", true, t),
+					"60-80": testShardInfo("ks_sharded", "60", "80", true, t),
+					"80-a0": testShardInfo("ks_sharded", "80", "a0", true, t),
+					"a0-c0": testShardInfo("ks_sharded", "a0", "c0", true, t),
+					"c0-e0": testShardInfo("ks_sharded", "c0", "e0", true, t),
+					"e0-":   testShardInfo("ks_sharded", "e0", "", true, t),
+					// Some non-serving shards below - these should never be in the output of vtexplain
+					"-80": testShardInfo("ks_sharded", "", "80", false, t),
+					"80-": testShardInfo("ks_sharded", "80", "", false, t),
 				},
 			},
 		},
@@ -321,11 +324,15 @@ func TestUsingKeyspaceShardMap(t *testing.T) {
 				// Have mercy on the poor soul that has this keyspace sharding.
 				// But, hey, vtexplain still works so they have that going for them.
 				"ks_sharded": {
-					"-80":   testShardInfo("ks_sharded", "", "80", t),
-					"80-90": testShardInfo("ks_sharded", "80", "90", t),
-					"90-a0": testShardInfo("ks_sharded", "90", "a0", t),
-					"a0-e8": testShardInfo("ks_sharded", "a0", "e8", t),
-					"e8-":   testShardInfo("ks_sharded", "e8", "", t),
+					"-80":   testShardInfo("ks_sharded", "", "80", true, t),
+					"80-90": testShardInfo("ks_sharded", "80", "90", true, t),
+					"90-a0": testShardInfo("ks_sharded", "90", "a0", true, t),
+					"a0-e8": testShardInfo("ks_sharded", "a0", "e8", true, t),
+					"e8-":   testShardInfo("ks_sharded", "e8", "", true, t),
+					// Plus some un-even shards that are not serving and which should never be in the output of vtexplain
+					"80-a0": testShardInfo("ks_sharded", "80", "a0", false, t),
+					"a0-a5": testShardInfo("ks_sharded", "a0", "a5", false, t),
+					"a5-":   testShardInfo("ks_sharded", "a5", "", false, t),
 				},
 			},
 		},
diff --git a/go/vt/vtgate/planbuilder/testdata/aggr_cases.json b/go/vt/vtgate/planbuilder/testdata/aggr_cases.json
index 45a6cb7238..760a136eca 100644
--- a/go/vt/vtgate/planbuilder/testdata/aggr_cases.json
+++ b/go/vt/vtgate/planbuilder/testdata/aggr_cases.json
@@ -4324,7 +4324,7 @@
           {
             "OperatorType": "Projection",
             "Expressions": [
-              "[COLUMN 2] * [COLUMN 3] as col",
+              "[COLUMN 2] as col",
               "[COLUMN 1]",
               "[COLUMN 0] as id"
             ],
@@ -4332,7 +4332,7 @@
               {
                 "OperatorType": "Join",
                 "Variant": "Join",
-                "JoinColumnIndexes": "L:1,L:2,L:0,R:1",
+                "JoinColumnIndexes": "L:1,L:2,L:0",
                 "JoinVars": {
                   "user_col": 0
                 },
@@ -4357,8 +4357,8 @@
                       "Name": "user",
                       "Sharded": true
                     },
-                    "FieldQuery": "select 1, count(*) from user_extra where 1 != 1 group by 1",
-                    "Query": "select 1, count(*) from user_extra where user_extra.col = :user_col group by 1",
+                    "FieldQuery": "select 1 from user_extra where 1 != 1 group by 1",
+                    "Query": "select 1 from user_extra where user_extra.col = :user_col group by 1",
                     "Table": "user_extra"
                   }
                 ]
@@ -4473,7 +4473,7 @@
           {
             "OperatorType": "Projection",
             "Expressions": [
-              "[COLUMN 2] * [COLUMN 3] as id",
+              "[COLUMN 2] as id",
               "[COLUMN 1]",
               "[COLUMN 0] as id"
             ],
@@ -4481,7 +4481,7 @@
               {
                 "OperatorType": "Join",
                 "Variant": "Join",
-                "JoinColumnIndexes": "L:0,L:1,L:0,R:1",
+                "JoinColumnIndexes": "L:0,L:1,L:0",
                 "TableName": "`user`_user_extra",
                 "Inputs": [
                   {
@@ -4503,8 +4503,8 @@
                       "Name": "user",
                       "Sharded": true
                     },
-                    "FieldQuery": "select 1, count(*) from user_extra where 1 != 1 group by 1",
-                    "Query": "select 1, count(*) from user_extra group by 1",
+                    "FieldQuery": "select 1 from user_extra where 1 != 1 group by 1",
+                    "Query": "select 1 from user_extra group by 1",
                     "Table": "user_extra"
                   }
                 ]
@@ -4819,5 +4819,82 @@
         "user.user"
       ]
     }
+  },
+  {
+    "comment": "AggregateRandom in non full group by query",
+    "query": "select u.id, u.name, count(m.predef1) from user.user as u join user.user_extra as m on u.id = m.order group by u.id",
+    "v3-plan": "unsupported: cross-shard query with aggregates",
+    "gen4-plan": {
+      "QueryType": "SELECT",
+      "Original": "select u.id, u.name, count(m.predef1) from user.user as u join user.user_extra as m on u.id = m.order group by u.id",
+      "Instructions": {
+        "OperatorType": "Aggregate",
+        "Variant": "Ordered",
+        "Aggregates": "random(1) AS name, sum_count(2) AS count(m.predef1)",
+        "GroupBy": "(0|3)",
+        "ResultColumns": 3,
+        "Inputs": [
+          {
+            "OperatorType": "Projection",
+            "Expressions": [
+              "[COLUMN 0] as id",
+              "[COLUMN 2] as name",
+              "[COLUMN 3] * [COLUMN 4] as count(m.predef1)",
+              "[COLUMN 1]"
+            ],
+            "Inputs": [
+              {
+                "OperatorType": "Sort",
+                "Variant": "Memory",
+                "OrderBy": "(0|1) ASC",
+                "Inputs": [
+                  {
+                    "OperatorType": "Join",
+                    "Variant": "Join",
+                    "JoinColumnIndexes": "R:2,R:3,R:0,L:1,R:1",
+                    "JoinVars": {
+                      "m_order": 0
+                    },
+                    "TableName": "user_extra_`user`",
+                    "Inputs": [
+                      {
+                        "OperatorType": "Route",
+                        "Variant": "Scatter",
+                        "Keyspace": {
+                          "Name": "user",
+                          "Sharded": true
+                        },
+                        "FieldQuery": "select m.`order`, count(m.predef1), weight_string(m.`order`) from user_extra as m where 1 != 1 group by m.`order`, weight_string(m.`order`)",
+                        "Query": "select m.`order`, count(m.predef1), weight_string(m.`order`) from user_extra as m group by m.`order`, weight_string(m.`order`)",
+                        "Table": "user_extra"
+                      },
+                      {
+                        "OperatorType": "Route",
+                        "Variant": "EqualUnique",
+                        "Keyspace": {
+                          "Name": "user",
+                          "Sharded": true
+                        },
+                        "FieldQuery": "select u.`name`, count(*), u.id, weight_string(u.id) from `user` as u where 1 != 1 group by u.id, weight_string(u.id)",
+                        "Query": "select u.`name`, count(*), u.id, weight_string(u.id) from `user` as u where u.id = :m_order group by u.id, weight_string(u.id)",
+                        "Table": "`user`",
+                        "Values": [
+                          ":m_order"
+                        ],
+                        "Vindex": "user_index"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      "TablesUsed": [
+        "user.user",
+        "user.user_extra"
+      ]
+    }
   }
 ]
\ No newline at end of file
diff --git a/go/vt/vttablet/onlineddl/executor_test.go b/go/vt/vttablet/onlineddl/executor_test.go
index c41f3b24f3..2d9be7630f 100644
--- a/go/vt/vttablet/onlineddl/executor_test.go
+++ b/go/vt/vttablet/onlineddl/executor_test.go
@@ -93,7 +93,7 @@ func TestValidateAndEditCreateTableStatement(t *testing.T) {
 			}, createTable)
 			assert.NoError(t, err)
 			assert.Equal(t, tc.countConstraints, len(uniqueConstraintNames))
-			assert.Equal(t, tc.countConstraints, len(constraintMap))
+			assert.Equalf(t, tc.countConstraints, len(constraintMap), "got contraints: %v", constraintMap)
 		})
 	}
 }
@@ -120,6 +120,18 @@ func TestValidateAndEditAlterTableStatement(t *testing.T) {
 			alter:  "alter table t add fulltext key name0_ft (name0), add column i int, add fulltext key name1_ft (name1), add fulltext key name2_ft (name2)",
 			expect: []string{"alter table t add fulltext key name0_ft (name0), add column i int", "alter table t add fulltext key name1_ft (name1)", "alter table t add fulltext key name2_ft (name2)"},
 		},
+		{
+			alter:  "alter table t add constraint check (id != 1)",
+			expect: []string{"alter table t add constraint chk_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
+		},
+		{
+			alter:  "alter table t add constraint t_chk_1 check (id != 1)",
+			expect: []string{"alter table t add constraint chk_1_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
+		},
+		{
+			alter:  "alter table t add constraint some_check check (id != 1)",
+			expect: []string{"alter table t add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
+		},
 	}
 	for _, tc := range tt {
 		t.Run(tc.alter, func(t *testing.T) {
@@ -129,7 +141,8 @@ func TestValidateAndEditAlterTableStatement(t *testing.T) {
 			require.True(t, ok)
 
 			m := map[string]string{}
-			alters, err := e.validateAndEditAlterTableStatement(context.Background(), alterTable, m)
+			onlineDDL := &schema.OnlineDDL{UUID: "a5a563da_dc1a_11ec_a416_0a43f95f28a3", Table: "t"}
+			alters, err := e.validateAndEditAlterTableStatement(context.Background(), onlineDDL, alterTable, m)
 			assert.NoError(t, err)
 			altersStrings := []string{}
 			for _, alter := range alters {
diff --git a/test/ci_workflow_gen.go b/test/ci_workflow_gen.go
index 7476d5bba8..5c6fbda247 100644
--- a/test/ci_workflow_gen.go
+++ b/test/ci_workflow_gen.go
@@ -73,13 +73,12 @@ var (
 		"ers_prs_newfeatures_heavy",
 		"15",
 		"vtgate_general_heavy",
-		"vtbackup_transform",
+		"vtbackup",
 		"18",
 		"xb_backup",
 		"21",
 		"22",
 		"mysql_server_vault",
-		"26",
 		"vstream_failover",
 		"vstream_stoponreshard_true",
 		"vstream_stoponreshard_false",
diff --git a/test/config.json b/test/config.json
index b2be584d6c..7b0b8245f1 100644
--- a/test/config.json
+++ b/test/config.json
@@ -114,28 +114,10 @@
 			"Args": ["vitess.io/vitess/go/test/endtoend/backup/vtbackup", "-timeout", "20m"],
 			"Command": [],
 			"Manual": false,
-			"Shard": "vtbackup_transform",
+			"Shard": "vtbackup",
 			"RetryMax": 1,
 			"Tags": ["upgrade_downgrade_backups"]
 		},
-		"backup_transform": {
-			"File": "unused.go",
-			"Args": ["vitess.io/vitess/go/test/endtoend/backup/transform", "-timeout", "15m"],
-			"Command": [],
-			"Manual": false,
-			"Shard": "vtbackup_transform",
-			"RetryMax": 1,
-			"Tags": ["upgrade_downgrade_backups"]
-		},
-		"backup_transform_mysqlctld": {
-			"File": "unused.go",
-			"Args": ["vitess.io/vitess/go/test/endtoend/backup/transform/mysqlctld"],
-			"Command": [],
-			"Manual": false,
-			"Shard": "21", 
-			"RetryMax": 1,
-			"Tags": []
-		},
 		"backup_xtrabackup": {
 			"File": "unused.go",
 			"Args": ["vitess.io/vitess/go/test/endtoend/backup/xtrabackup"],
@@ -369,17 +351,6 @@
 				"site_test"
 			]
 		},
-		"pitrtls": {
-			"File": "unused.go",
-			"Args": ["vitess.io/vitess/go/test/endtoend/recovery/pitrtls"],
-			"Command": [],
-			"Manual": false,
-			"Shard": "26",
-			"RetryMax": 1,
-			"Tags": [
-				"site_test"
-			]
-		},
 		"recovery": {
 			"File": "unused.go",
 			"Args": ["vitess.io/vitess/go/test/endtoend/recovery/unshardedrecovery"],