Repository: vitessio/vitess
Pull request #5558 (2019-12-12 20:28:13)

Add fix for SHOW FULL COLUMNS when query has a qualifier
This addresses #5393 
Signed-off-by: Saif Alharthi <saif@saifalharthi.me>

morgo: @saifalharthi do you mind rebasing on master? We just changed some of the tests to be required-to-pass, but the names are not lining up due to https://github.com/vitessio/vitess/pull/5561

Patch:
diff --git a/.dockerignore b/.dockerignore
index 58354ff421..5b123fd019 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,8 +5,3 @@ java/*/target
 java/*/bin
 php/vendor
 releases
-/dist/
-/py-vtdb/
-/vthook/
-/bin/
-/vtdataroot/
diff --git a/.github/workflows/check_make_parser.yml b/.github/workflows/check_make_parser.yml
index c46f5bffc5..0657236fc3 100644
--- a/.github/workflows/check_make_parser.yml
+++ b/.github/workflows/check_make_parser.yml
@@ -3,7 +3,7 @@ on: [push, pull_request]
 jobs:
 
   build:
-    name: Check Make Parser
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget
         sudo service mysql stop
         sudo service etcd stop
diff --git a/.github/workflows/cluster_endtoend.yml b/.github/workflows/cluster_endtoend.yml
index 448622241a..a2f3f9d7d3 100644
--- a/.github/workflows/cluster_endtoend.yml
+++ b/.github/workflows/cluster_endtoend.yml
@@ -3,7 +3,7 @@ on: [push, pull_request]
 jobs:
 
   build:
-    name: Cluster End-to-End Test
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget
         sudo service mysql stop
         sudo service etcd stop
diff --git a/.github/workflows/endtoend.yml b/.github/workflows/endtoend.yml
index 7a0c594ee3..6ebec31c3b 100644
--- a/.github/workflows/endtoend.yml
+++ b/.github/workflows/endtoend.yml
@@ -3,7 +3,7 @@ on: [push, pull_request]
 jobs:
 
   build:
-    name: End-to-End Test
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget
         sudo service mysql stop
         sudo service etcd stop
diff --git a/.github/workflows/local_example.yml b/.github/workflows/local_example.yml
index 496229ade1..fe0035ec10 100644
--- a/.github/workflows/local_example.yml
+++ b/.github/workflows/local_example.yml
@@ -3,7 +3,7 @@ on: [push, pull_request]
 jobs:
 
   build:
-    name: Local Example Test
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget
         sudo service mysql stop
         sudo service etcd stop
diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml
index db69c68cf2..dd53b59a1d 100644
--- a/.github/workflows/unit.yml
+++ b/.github/workflows/unit.yml
@@ -3,7 +3,7 @@ on: [push, pull_request]
 jobs:
 
   build:
-    name: Unit Test
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget ant openjdk-8-jdk
         sudo service mysql stop
         sudo service etcd stop
diff --git a/.github/workflows/unit_race.yml b/.github/workflows/unit_race.yml
index 7da452685d..a85a6c87ef 100644
--- a/.github/workflows/unit_race.yml
+++ b/.github/workflows/unit_race.yml
@@ -3,7 +3,7 @@ on: [push]
 jobs:
 
   build:
-    name: Unit Test (Race)
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget
         sudo service mysql stop
         sudo service etcd stop
diff --git a/.gitignore b/.gitignore
index be8fe72756..2561bdd937 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,8 +80,9 @@ releases
 # Vagrant
 .vagrant
 
-/dist/
-/py-vtdb
-/vthook/
-/bin/
-/vtdataroot/
+dist/*
+py-vtdb*
+vthook*
+bin*
+
+vtdataroot*
diff --git a/docker/bootstrap/Dockerfile.mariadb b/docker/bootstrap/Dockerfile.mariadb
index 19d5c9973d..e0f6106837 100644
--- a/docker/bootstrap/Dockerfile.mariadb
+++ b/docker/bootstrap/Dockerfile.mariadb
@@ -22,6 +22,5 @@ RUN for i in $(seq 1 10); do apt-key adv --no-tty --keyserver keys.gnupg.net --r
 # Bootstrap Vitess
 WORKDIR /vt/src/vitess.io/vitess
 
-ENV MYSQL_FLAVOR MariaDB
 USER vitess
 RUN ./bootstrap.sh
diff --git a/docker/bootstrap/Dockerfile.mariadb103 b/docker/bootstrap/Dockerfile.mariadb103
index c2828ddd25..024fe6a80b 100644
--- a/docker/bootstrap/Dockerfile.mariadb103
+++ b/docker/bootstrap/Dockerfile.mariadb103
@@ -11,6 +11,5 @@ RUN apt-key adv --no-tty --recv-keys --keyserver keyserver.ubuntu.com 0xF1656F24
 # Bootstrap Vitess
 WORKDIR /vt/src/vitess.io/vitess
 
-ENV MYSQL_FLAVOR MariaDB103
 USER vitess
 RUN ./bootstrap.sh
diff --git a/docker/bootstrap/Dockerfile.mysql80 b/docker/bootstrap/Dockerfile.mysql80
index 0523a7847c..e6d00bc524 100644
--- a/docker/bootstrap/Dockerfile.mysql80
+++ b/docker/bootstrap/Dockerfile.mysql80
@@ -17,6 +17,5 @@ RUN for i in $(seq 1 10); do apt-key adv --no-tty --recv-keys --keyserver ha.poo
 # Bootstrap Vitess
 WORKDIR /vt/src/vitess.io/vitess
 
-ENV MYSQL_FLAVOR MySQL80
 USER vitess
-RUN ./bootstrap.sh
+RUN ./bootstrap.sh
\ No newline at end of file
diff --git a/docker/bootstrap/Dockerfile.percona80 b/docker/bootstrap/Dockerfile.percona80
index 1ce9c52103..c5ce5d5ee4 100644
--- a/docker/bootstrap/Dockerfile.percona80
+++ b/docker/bootstrap/Dockerfile.percona80
@@ -32,6 +32,5 @@ RUN for i in $(seq 1 10); do apt-key adv --no-tty --keyserver keys.gnupg.net --r
 # Bootstrap Vitess
 WORKDIR /vt/src/vitess.io/vitess
 
-ENV MYSQL_FLAVOR MySQL80
 USER vitess
 RUN ./bootstrap.sh
diff --git a/docker/lite/Dockerfile.alpine b/docker/lite/Dockerfile.alpine
index f57b9d7a8f..b8fc9467f9 100644
--- a/docker/lite/Dockerfile.alpine
+++ b/docker/lite/Dockerfile.alpine
@@ -25,7 +25,6 @@ RUN echo '@edge http://nl.alpinelinux.org/alpine/edge/main' >> /etc/apk/reposito
 ENV VTROOT /vt/src/vitess.io/vitess
 ENV VTDATAROOT /vt/vtdataroot
 ENV PATH $VTROOT/bin:$PATH
-ENV MYSQL_FLAVOR MariaDB103
 
 # Create vitess user
 RUN addgroup -S vitess && adduser -S -G vitess vitess && mkdir -p /vt
diff --git a/docker/lite/Dockerfile.mariadb b/docker/lite/Dockerfile.mariadb
index 6907534c03..77eddfd800 100644
--- a/docker/lite/Dockerfile.mariadb
+++ b/docker/lite/Dockerfile.mariadb
@@ -36,7 +36,6 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
 ENV VTROOT /vt/src/vitess.io/vitess
 ENV VTDATAROOT /vt/vtdataroot
 ENV PATH $VTROOT/bin:$PATH
-ENV MYSQL_FLAVOR MariaDB
 
 # Copy binaries (placed by build.sh)
 COPY --from=staging /vt/ /vt/
diff --git a/docker/lite/Dockerfile.mariadb103 b/docker/lite/Dockerfile.mariadb103
index cd962803f7..a1d37b4c42 100644
--- a/docker/lite/Dockerfile.mariadb103
+++ b/docker/lite/Dockerfile.mariadb103
@@ -35,7 +35,6 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
 ENV VTROOT /vt/src/vitess.io/vitess
 ENV VTDATAROOT /vt/vtdataroot
 ENV PATH $VTROOT/bin:$PATH
-ENV MYSQL_FLAVOR MariaDB103
 
 # Copy binaries (placed by build.sh)
 COPY --from=staging /vt/ /vt/
diff --git a/docker/lite/Dockerfile.mysql80 b/docker/lite/Dockerfile.mysql80
index 85383be648..0fade69bd3 100644
--- a/docker/lite/Dockerfile.mysql80
+++ b/docker/lite/Dockerfile.mysql80
@@ -39,7 +39,6 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
 ENV VTROOT /vt/src/vitess.io/vitess
 ENV VTDATAROOT /vt/vtdataroot
 ENV PATH $VTROOT/bin:$PATH
-ENV MYSQL_FLAVOR MySQL80
 
 # Copy binaries (placed by build.sh)
 COPY --from=staging /vt/ /vt/
diff --git a/docker/lite/Dockerfile.percona80 b/docker/lite/Dockerfile.percona80
index 98b4b30a57..5d872499cf 100644
--- a/docker/lite/Dockerfile.percona80
+++ b/docker/lite/Dockerfile.percona80
@@ -44,7 +44,6 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
 ENV VTROOT /vt/src/vitess.io/vitess
 ENV VTDATAROOT /vt/vtdataroot
 ENV PATH $VTROOT/bin:$PATH
-ENV MYSQL_FLAVOR MySQL80
 
 # Copy binaries (placed by build.sh)
 COPY --from=staging /vt/ /vt/
diff --git a/go.mod b/go.mod
index 6308bc259c..af52af7313 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
 	github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 // indirect
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
-	github.com/evanphx/json-patch v4.5.0+incompatible // indirect
+	github.com/evanphx/json-patch v4.5.0+incompatible
 	github.com/ghodss/yaml v0.0.0-20161207003320-04f313413ffd // indirect
 	github.com/go-ini/ini v1.12.0 // indirect
 	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
@@ -25,6 +25,7 @@ require (
 	github.com/golang/mock v1.3.1
 	github.com/golang/protobuf v1.3.2
 	github.com/golang/snappy v0.0.0-20170215233205-553a64147049
+	github.com/google/btree v1.0.0 // indirect
 	github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf // indirect
 	github.com/gorilla/websocket v0.0.0-20160912153041-2d1e4548da23
 	github.com/grpc-ecosystem/go-grpc-middleware v1.1.0
@@ -44,11 +45,13 @@ require (
 	github.com/klauspost/crc32 v1.2.0 // indirect
 	github.com/klauspost/pgzip v1.2.0
 	github.com/kr/pretty v0.1.0 // indirect
-	github.com/krishicks/yaml-patch v0.0.10 // indirect
+	github.com/krishicks/yaml-patch v0.0.10
 	github.com/mattn/go-runewidth v0.0.1 // indirect
 	github.com/minio/minio-go v0.0.0-20190131015406-c8a261de75c1
 	github.com/mitchellh/go-testing-interface v1.0.0 // indirect
 	github.com/mitchellh/mapstructure v1.1.2 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.1 // indirect
 	github.com/olekukonko/tablewriter v0.0.0-20160115111002-cca8bbc07984
 	github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02
 	github.com/opentracing/opentracing-go v1.1.0
diff --git a/go/vt/servenv/status.go b/go/vt/servenv/status.go
index c9fdbfb3fd..f391bf470a 100644
--- a/go/vt/servenv/status.go
+++ b/go/vt/servenv/status.go
@@ -104,7 +104,6 @@ Started: {{.StartTime}}<br>
 Running on {{.Hostname}}<br>
 View <a href=/debug/vars>variables</a>,
      <a href=/debug/pprof>debugging profiles</a>,
-     <a href=/debug/scatter_stats>scatter query statistics</a>,
 </div>
 </div>`
 
diff --git a/go/vt/sqlparser/comments.go b/go/vt/sqlparser/comments.go
index afffd326c8..26c664cf91 100644
--- a/go/vt/sqlparser/comments.go
+++ b/go/vt/sqlparser/comments.go
@@ -123,9 +123,7 @@ func SplitMarginComments(sql string) (query string, comments MarginComments) {
 		Leading:  strings.TrimLeftFunc(sql[:leadingEnd], unicode.IsSpace),
 		Trailing: strings.TrimRightFunc(sql[trailingStart:], unicode.IsSpace),
 	}
-	return strings.TrimFunc(sql[leadingEnd:trailingStart], func(c rune) bool {
-		return unicode.IsSpace(c) || c == ';'
-	}), comments
+	return strings.TrimFunc(sql[leadingEnd:trailingStart], unicode.IsSpace), comments
 }
 
 // StripLeadingComments trims the SQL string and removes any leading comments
diff --git a/go/vt/vitessdriver/doc.go b/go/vt/vitessdriver/doc.go
index 31ea98651b..a3e2770b8a 100644
--- a/go/vt/vitessdriver/doc.go
+++ b/go/vt/vitessdriver/doc.go
@@ -28,12 +28,13 @@ Example
 Using this SQL driver is as simple as:
 
   import (
+    "time"
     "vitess.io/vitess/go/vt/vitessdriver"
   )
 
   func main() {
     // Connect to vtgate.
-    db, err := vitessdriver.Open("localhost:15991", "@master")
+    db, err := vitessdriver.Open("localhost:15991", "keyspace", "master", 30*time.Second)
 
     // Use "db" via the Golang sql interface.
   }
diff --git a/go/vt/vtgate/engine/delete.go b/go/vt/vtgate/engine/delete.go
index b0f06174c6..22e2fd61a5 100644
--- a/go/vt/vtgate/engine/delete.go
+++ b/go/vt/vtgate/engine/delete.go
@@ -67,9 +67,6 @@ type Delete struct {
 
 	// QueryTimeout contains the optional timeout (in milliseconds) to apply to this query
 	QueryTimeout int
-
-	// Delete does not take inputs
-	noInputs
 }
 
 // MarshalJSON serializes the Delete into a JSON representation.
diff --git a/go/vt/vtgate/engine/insert.go b/go/vt/vtgate/engine/insert.go
index ec2a54f106..bf75e6a446 100644
--- a/go/vt/vtgate/engine/insert.go
+++ b/go/vt/vtgate/engine/insert.go
@@ -80,9 +80,6 @@ type Insert struct {
 
 	// QueryTimeout contains the optional timeout (in milliseconds) to apply to this query
 	QueryTimeout int
-
-	// Insert does not take inputs
-	noInputs
 }
 
 // NewQueryInsert creates an Insert with a query string.
diff --git a/go/vt/vtgate/engine/join.go b/go/vt/vtgate/engine/join.go
index e577986605..3cfb25e7a1 100644
--- a/go/vt/vtgate/engine/join.go
+++ b/go/vt/vtgate/engine/join.go
@@ -171,11 +171,6 @@ func (jn *Join) GetFields(vcursor VCursor, bindVars map[string]*querypb.BindVari
 	return result, nil
 }
 
-// Inputs returns the input primitives for this join
-func (jn *Join) Inputs() []Primitive {
-	return []Primitive{jn.Left, jn.Right}
-}
-
 func joinFields(lfields, rfields []*querypb.Field, cols []int) []*querypb.Field {
 	fields := make([]*querypb.Field, len(cols))
 	for i, index := range cols {
diff --git a/go/vt/vtgate/engine/limit.go b/go/vt/vtgate/engine/limit.go
index 410a5af624..65b6b39da3 100644
--- a/go/vt/vtgate/engine/limit.go
+++ b/go/vt/vtgate/engine/limit.go
@@ -161,11 +161,6 @@ func (l *Limit) GetFields(vcursor VCursor, bindVars map[string]*querypb.BindVari
 	return l.Input.GetFields(vcursor, bindVars)
 }
 
-// Inputs returns the input to limit
-func (l *Limit) Inputs() []Primitive {
-	return []Primitive{l.Input}
-}
-
 func (l *Limit) fetchCount(bindVars map[string]*querypb.BindVariable) (int, error) {
 	resolved, err := l.Count.ResolveValue(bindVars)
 	if err != nil {
diff --git a/go/vt/vtgate/engine/memory_sort.go b/go/vt/vtgate/engine/memory_sort.go
index f426eb2039..b9832b34e5 100644
--- a/go/vt/vtgate/engine/memory_sort.go
+++ b/go/vt/vtgate/engine/memory_sort.go
@@ -78,7 +78,7 @@ func (ms *MemorySort) SetTruncateColumnCount(count int) {
 	ms.TruncateColumnCount = count
 }
 
-// Execute satisfies the Primitive interface.
+// Execute satisfies the Primtive interface.
 func (ms *MemorySort) Execute(vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool) (*sqltypes.Result, error) {
 	count, err := ms.fetchCount(bindVars)
 	if err != nil {
@@ -105,7 +105,7 @@ func (ms *MemorySort) Execute(vcursor VCursor, bindVars map[string]*querypb.Bind
 	return result.Truncate(ms.TruncateColumnCount), nil
 }
 
-// StreamExecute satisfies the Primitive interface.
+// StreamExecute satisfies the Primtive interface.
 func (ms *MemorySort) StreamExecute(vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error {
 	count, err := ms.fetchCount(bindVars)
 	if err != nil {
@@ -155,16 +155,11 @@ func (ms *MemorySort) StreamExecute(vcursor VCursor, bindVars map[string]*queryp
 	return cb(&sqltypes.Result{Rows: sh.rows})
 }
 
-// GetFields satisfies the Primitive interface.
+// GetFields satisfies the Primtive interface.
 func (ms *MemorySort) GetFields(vcursor VCursor, bindVars map[string]*querypb.BindVariable) (*sqltypes.Result, error) {
 	return ms.Input.GetFields(vcursor, bindVars)
 }
 
-// Inputs returns the input to memory sort
-func (ms *MemorySort) Inputs() []Primitive {
-	return []Primitive{ms.Input}
-}
-
 func (ms *MemorySort) fetchCount(bindVars map[string]*querypb.BindVariable) (int, error) {
 	resolved, err := ms.UpperLimit.ResolveValue(bindVars)
 	if err != nil {
diff --git a/go/vt/vtgate/engine/ordered_aggregate.go b/go/vt/vtgate/engine/ordered_aggregate.go
index 9ec98b35d2..9abe441921 100644
--- a/go/vt/vtgate/engine/ordered_aggregate.go
+++ b/go/vt/vtgate/engine/ordered_aggregate.go
@@ -309,11 +309,6 @@ func (oa *OrderedAggregate) GetFields(vcursor VCursor, bindVars map[string]*quer
 	return qr.Truncate(oa.TruncateColumnCount), nil
 }
 
-// Inputs returns the Primitive input for this aggregation
-func (oa *OrderedAggregate) Inputs() []Primitive {
-	return []Primitive{oa.Input}
-}
-
 func (oa *OrderedAggregate) keysEqual(row1, row2 []sqltypes.Value) (bool, error) {
 	for _, key := range oa.Keys {
 		cmp, err := sqltypes.NullsafeCompare(row1[key], row2[key])
diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go
index b69d6ea373..cb9c916118 100644
--- a/go/vt/vtgate/engine/primitive.go
+++ b/go/vt/vtgate/engine/primitive.go
@@ -119,28 +119,6 @@ func (p *Plan) Stats() (execCount uint64, execTime time.Duration, shardQueries,
 	return
 }
 
-// Match is used to check if a Primitive matches
-type Match func(node Primitive) bool
-
-// Find will return the first Primitive that matches the evaluate function. If no match is found, nil will be returned
-func Find(isMatch Match, start Primitive) Primitive {
-	if isMatch(start) {
-		return start
-	}
-	for _, input := range start.Inputs() {
-		result := Find(isMatch, input)
-		if result != nil {
-			return result
-		}
-	}
-	return nil
-}
-
-// Exists traverses recursively down the Primitive tree structure, and returns true when Match returns true
-func Exists(m Match, p Primitive) bool {
-	return Find(m, p) != nil
-}
-
 // Size is defined so that Plan can be given to a cache.LRUCache.
 // VTGate needs to maintain a cache of plans. It uses LRUCache, which
 // in turn requires its objects to define a Size function.
@@ -148,10 +126,8 @@ func (p *Plan) Size() int {
 	return 1
 }
 
-// Primitive is the building block of the engine execution plan. They form a tree structure, where the leaves typically
-// issue queries to one or more vttablet.
-// During execution, the Primitive's pass Result objects up the tree structure, until reaching the root,
-// and its result is passed to the client.
+// Primitive is the interface that needs to be satisfied by
+// all primitives of a plan.
 type Primitive interface {
 	RouteType() string
 	GetKeyspaceName() string
@@ -159,14 +135,4 @@ type Primitive interface {
 	Execute(vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool) (*sqltypes.Result, error)
 	StreamExecute(vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantields bool, callback func(*sqltypes.Result) error) error
 	GetFields(vcursor VCursor, bindVars map[string]*querypb.BindVariable) (*sqltypes.Result, error)
-
-	// The inputs to this Primitive
-	Inputs() []Primitive
-}
-
-type noInputs struct{}
-
-// Inputs implements no inputs
-func (noInputs) Inputs() []Primitive {
-	return nil
 }
diff --git a/go/vt/vtgate/engine/pullout_subquery.go b/go/vt/vtgate/engine/pullout_subquery.go
index ade790599a..b632d37359 100644
--- a/go/vt/vtgate/engine/pullout_subquery.go
+++ b/go/vt/vtgate/engine/pullout_subquery.go
@@ -38,11 +38,6 @@ type PulloutSubquery struct {
 	Underlying     Primitive
 }
 
-// Inputs returns the input primitives for this join
-func (ps *PulloutSubquery) Inputs() []Primitive {
-	return []Primitive{ps.Subquery, ps.Underlying}
-}
-
 // RouteType returns a description of the query routing type used by the primitive
 func (ps *PulloutSubquery) RouteType() string {
 	return ps.Opcode.String()
diff --git a/go/vt/vtgate/engine/route.go b/go/vt/vtgate/engine/route.go
index 9d5b45471b..4576894f13 100644
--- a/go/vt/vtgate/engine/route.go
+++ b/go/vt/vtgate/engine/route.go
@@ -84,9 +84,6 @@ type Route struct {
 
 	// ScatterErrorsAsWarnings is true if results should be returned even if some shards have an error
 	ScatterErrorsAsWarnings bool
-
-	// Route does not take inputs
-	noInputs
 }
 
 // NewSimpleRoute creates a Route with the bare minimum of parameters.
diff --git a/go/vt/vtgate/engine/subquery.go b/go/vt/vtgate/engine/subquery.go
index fc37cd9b3e..b9e28885dd 100644
--- a/go/vt/vtgate/engine/subquery.go
+++ b/go/vt/vtgate/engine/subquery.go
@@ -71,11 +71,6 @@ func (sq *Subquery) GetFields(vcursor VCursor, bindVars map[string]*querypb.Bind
 	return &sqltypes.Result{Fields: sq.buildFields(inner)}, nil
 }
 
-// Inputs returns the input to this primitive
-func (sq *Subquery) Inputs() []Primitive {
-	return []Primitive{sq.Subquery}
-}
-
 // buildResult builds a new result by pulling the necessary columns from
 // the subquery in the requested order.
 func (sq *Subquery) buildResult(inner *sqltypes.Result) *sqltypes.Result {
diff --git a/go/vt/vtgate/engine/update.go b/go/vt/vtgate/engine/update.go
index 49ee8324b9..dc3c591546 100644
--- a/go/vt/vtgate/engine/update.go
+++ b/go/vt/vtgate/engine/update.go
@@ -70,9 +70,6 @@ type Update struct {
 
 	// QueryTimeout contains the optional timeout (in milliseconds) to apply to this query
 	QueryTimeout int
-
-	// Update does not take inputs
-	noInputs
 }
 
 // MarshalJSON serializes the Update into a JSON representation.
diff --git a/go/vt/vtgate/engine/vindex_func.go b/go/vt/vtgate/engine/vindex_func.go
index bc1aaf61da..8ffb591405 100644
--- a/go/vt/vtgate/engine/vindex_func.go
+++ b/go/vt/vtgate/engine/vindex_func.go
@@ -39,9 +39,6 @@ type VindexFunc struct {
 	// TODO(sougou): add support for MultiColumn.
 	Vindex vindexes.SingleColumn
 	Value  sqltypes.PlanValue
-
-	// VindexFunc does not take inputs
-	noInputs
 }
 
 // MarshalJSON serializes the VindexFunc into a JSON representation.
diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go
index 7a91df5e65..5499160dab 100644
--- a/go/vt/vtgate/executor.go
+++ b/go/vt/vtgate/executor.go
@@ -92,10 +92,6 @@ type Executor struct {
 
 var executorOnce sync.Once
 
-const pathQueryPlans = "/debug/query_plans"
-const pathScatterStats = "/debug/scatter_stats"
-const pathVSchema = "/debug/vschema"
-
 // NewExecutor creates a new Executor.
 func NewExecutor(ctx context.Context, serv srvtopo.Server, cell, statsName string, resolver *Resolver, normalize bool, streamSize int, queryPlanCacheSize int64) *Executor {
 	e := &Executor{
@@ -121,9 +117,8 @@ func NewExecutor(ctx context.Context, serv srvtopo.Server, cell, statsName strin
 		stats.Publish("QueryPlanCacheOldest", stats.StringFunc(func() string {
 			return fmt.Sprintf("%v", e.plans.Oldest())
 		}))
-		http.Handle(pathQueryPlans, e)
-		http.Handle(pathScatterStats, e)
-		http.Handle(pathVSchema, e)
+		http.Handle("/debug/query_plans", e)
+		http.Handle("/debug/vschema", e)
 	})
 	return e
 }
@@ -845,6 +840,14 @@ func (e *Executor) handleShow(ctx context.Context, safeSession *SafeSession, sql
 				}
 			}
 		}
+	case sqlparser.KeywordString(sqlparser.COLUMNS):
+		if !show.OnTable.Qualifier.IsEmpty() {
+			destKeyspace = show.OnTable.Qualifier.String()
+			show.OnTable.Qualifier = sqlparser.NewTableIdent("")
+		} else {
+			break
+		}
+		sql = sqlparser.String(show)
 	case sqlparser.KeywordString(sqlparser.TABLES):
 		if show.ShowTablesOpt != nil && show.ShowTablesOpt.DbName != "" {
 			if destKeyspace == "" {
@@ -1424,31 +1427,31 @@ func (e *Executor) ServeHTTP(response http.ResponseWriter, request *http.Request
 		acl.SendError(response, err)
 		return
 	}
-
-	switch request.URL.Path {
-	case pathQueryPlans:
-		returnAsJSON(response, e.plans.Items())
-	case pathVSchema:
-		returnAsJSON(response, e.VSchema())
-	case pathScatterStats:
-		e.WriteScatterStats(response)
-	default:
+	if request.URL.Path == "/debug/query_plans" {
+		response.Header().Set("Content-Type", "application/json; charset=utf-8")
+		buf, err := json.MarshalIndent(e.plans.Items(), "", " ")
+		if err != nil {
+			response.Write([]byte(err.Error()))
+			return
+		}
+		ebuf := bytes.NewBuffer(nil)
+		json.HTMLEscape(ebuf, buf)
+		response.Write(ebuf.Bytes())
+	} else if request.URL.Path == "/debug/vschema" {
+		response.Header().Set("Content-Type", "application/json; charset=utf-8")
+		b, err := json.MarshalIndent(e.VSchema(), "", " ")
+		if err != nil {
+			response.Write([]byte(err.Error()))
+			return
+		}
+		buf := bytes.NewBuffer(nil)
+		json.HTMLEscape(buf, b)
+		response.Write(buf.Bytes())
+	} else {
 		response.WriteHeader(http.StatusNotFound)
 	}
 }
 
-func returnAsJSON(response http.ResponseWriter, stuff interface{}) {
-	response.Header().Set("Content-Type", "application/json; charset=utf-8")
-	buf, err := json.MarshalIndent(stuff, "", " ")
-	if err != nil {
-		_, _ = response.Write([]byte(err.Error()))
-		return
-	}
-	ebuf := bytes.NewBuffer(nil)
-	json.HTMLEscape(ebuf, buf)
-	_, _ = response.Write(ebuf.Bytes())
-}
-
 // Plans returns the LRU plan cache
 func (e *Executor) Plans() *cache.LRUCache {
 	return e.plans
diff --git a/go/vt/vtgate/executor_scatter_stats.go b/go/vt/vtgate/executor_scatter_stats.go
deleted file mode 100644
index 24858564a5..0000000000
--- a/go/vt/vtgate/executor_scatter_stats.go
+++ /dev/null
@@ -1,184 +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 vtgate
-
-import (
-	"fmt"
-	"html/template"
-	"net/http"
-	"time"
-
-	"vitess.io/vitess/go/vt/logz"
-
-	"vitess.io/vitess/go/vt/proto/vtrpc"
-	"vitess.io/vitess/go/vt/vterrors"
-	"vitess.io/vitess/go/vt/vtgate/engine"
-)
-
-type statsResults struct {
-	TotalReadOnlyTime  time.Duration
-	TotalScatterTime   time.Duration
-	PercentTimeScatter float64
-	Items              []*statsResultItem
-}
-
-// using this intermediate data structure to make testing easier
-type statsResultItem struct {
-	Query                  string
-	AvgTimePerQuery        time.Duration
-	PercentTimeOfReads     float64
-	PercentTimeOfScatters  float64
-	PercentCountOfReads    float64
-	PercentCountOfScatters float64
-	From                   string
-	Count                  uint64
-}
-
-func (e *Executor) gatherScatterStats() (statsResults, error) {
-	scatterExecTime := time.Duration(0)
-	readOnlyTime := time.Duration(0)
-	scatterCount := uint64(0)
-	readOnlyCount := uint64(0)
-	totalExecTime := time.Duration(0)
-	totalCount := uint64(0)
-
-	plans := make([]*engine.Plan, 0)
-	routes := make([]*engine.Route, 0)
-	// First we go over all plans and collect statistics and all query plans for scatter queries
-	for _, item := range e.plans.Items() {
-		plan := item.Value.(*engine.Plan)
-
-		scatter := engine.Find(findScatter, plan.Instructions)
-		readOnly := !engine.Exists(isUpdating, plan.Instructions)
-		isScatter := scatter != nil
-
-		if isScatter {
-			route, isRoute := scatter.(*engine.Route)
-			if !isRoute {
-				return statsResults{}, vterrors.Errorf(vtrpc.Code_INTERNAL, "expected a route, but found a %v", scatter)
-			}
-			plans = append(plans, plan)
-			routes = append(routes, route)
-			scatterExecTime += plan.ExecTime
-			scatterCount += plan.ExecCount
-		}
-		if readOnly {
-			readOnlyTime += plan.ExecTime
-			readOnlyCount += plan.ExecCount
-		}
-
-		totalExecTime += plan.ExecTime
-		totalCount += plan.ExecCount
-	}
-
-	// Now we'll go over all scatter queries we've found and produce result items for each
-	resultItems := make([]*statsResultItem, len(plans))
-	for i, plan := range plans {
-		route := routes[i]
-		resultItems[i] = &statsResultItem{
-			Query:                  plan.Original,
-			AvgTimePerQuery:        time.Duration(plan.ExecTime.Nanoseconds() / int64(plan.ExecCount)),
-			PercentTimeOfReads:     float64(100 * plan.ExecTime / readOnlyTime),
-			PercentTimeOfScatters:  float64(100 * plan.ExecTime / scatterExecTime),
-			PercentCountOfReads:    float64(100 * plan.ExecCount / readOnlyCount),
-			PercentCountOfScatters: float64(100 * plan.ExecCount / scatterCount),
-			From:                   route.Keyspace.Name + "." + route.TableName,
-			Count:                  plan.ExecCount,
-		}
-	}
-	result := statsResults{
-		TotalReadOnlyTime:  readOnlyTime,
-		TotalScatterTime:   scatterExecTime,
-		PercentTimeScatter: 100 * float64(scatterExecTime) / float64(readOnlyTime),
-		Items:              resultItems,
-	}
-	return result, nil
-}
-
-// WriteScatterStats will write an html report to the provided response writer
-func (e *Executor) WriteScatterStats(w http.ResponseWriter) {
-	logz.StartHTMLTable(w)
-	defer logz.EndHTMLTable(w)
-
-	results, err := e.gatherScatterStats()
-	if err != nil {
-		http.Error(w, err.Error(), 500)
-		return
-	}
-
-	t := template.New("template")
-	t, err = t.Parse(html)
-	if err != nil {
-		http.Error(w, err.Error(), 500)
-		return
-	}
-
-	err = t.Execute(w, results)
-	if err != nil {
-		http.Error(w, err.Error(), 500)
-	}
-
-	_, err = w.Write([]byte(fmt.Sprintf("Percentage of time spent on scatter queries: %2.2f%%", results.PercentTimeScatter)))
-
-	if err != nil {
-		http.Error(w, err.Error(), 500)
-	}
-
-}
-
-const html = `
-<thead>
-	<tr>
-		<th>Query</th>
-		<th># of executions</th>
-		<th>Avg time/query</th>
-		<th>% time of reads</th>
-		<th>% time of scatters</th>
-		<th>% of reads</th>
-		<th>% of scatters</th>
-	</tr>
-</thead>
-{{range .Items}}
-<tr class="medium">
-	<td>{{.Query}}</td>
-	<td>{{.Count}}</td>
-	<td>{{.AvgTimePerQuery}}</td>
-	<td>{{.PercentTimeOfReads}}</td>
-	<td>{{.PercentTimeOfScatters}}</td>
-	<td>{{.PercentCountOfReads}}</td>
-	<td>{{.PercentCountOfScatters}}</td>
-</tr>
-{{end}}
-`
-
-func findScatter(p engine.Primitive) bool {
-	switch v := p.(type) {
-	case *engine.Route:
-		return v.Opcode == engine.SelectScatter
-	default:
-		return false
-	}
-}
-
-func isUpdating(p engine.Primitive) bool {
-	switch p.(type) {
-	case *engine.Update, *engine.Delete, *engine.Insert:
-		return true
-	default:
-		return false
-	}
-}
diff --git a/go/vt/vtgate/executor_stats.go b/go/vt/vtgate/executor_stats.go
index 8bb5a40bef..ffbc8412c1 100644
--- a/go/vt/vtgate/executor_stats.go
+++ b/go/vt/vtgate/executor_stats.go
@@ -33,7 +33,6 @@ const (
   <a href="/debug/querylogz">Current Query Log</a><br>
   <a href="/debug/queryz">Query Plan Stats</a><br>
   <a href="/debug/query_plans">Query Plans</a><br>
-  <a href="/debug/scatter_stats">Scatter Query Statistics</a><br>
 </td>
 </tr>
 </table>
diff --git a/go/vt/vtgate/planbuilder/route_option.go b/go/vt/vtgate/planbuilder/route_option.go
index fa3bab2c6a..1d6ba1c367 100644
--- a/go/vt/vtgate/planbuilder/route_option.go
+++ b/go/vt/vtgate/planbuilder/route_option.go
@@ -87,8 +87,12 @@ func (ro *routeOption) JoinCanMerge(pb *primitiveBuilder, rro *routeOption, ajoi
 }
 
 func (ro *routeOption) MergeJoin(rro *routeOption, isLeftJoin bool) {
-	ro.merge(rro)
 	ro.vschemaTable = nil
+	ro.substitutions = append(ro.substitutions, rro.substitutions...)
+	if isLeftJoin {
+		return
+	}
+	// Add RHS vindexes only if it's not a left join.
 	for c, v := range rro.vindexMap {
 		if ro.vindexMap == nil {
 			ro.vindexMap = make(map[*column]vindexes.SingleColumn)
@@ -97,16 +101,6 @@ func (ro *routeOption) MergeJoin(rro *routeOption, isLeftJoin bool) {
 	}
 }
 
-// merge merges two routeOptions. If the LHS (ro) is a SelectReference,
-// then the RHS option values supersede LHS.
-func (ro *routeOption) merge(rro *routeOption) {
-	if ro.eroute.Opcode == engine.SelectReference {
-		// Swap the values and then merge.
-		*ro, *rro = *rro, *ro
-	}
-	ro.substitutions = append(ro.substitutions, rro.substitutions...)
-}
-
 func (ro *routeOption) SubqueryCanMerge(pb *primitiveBuilder, inner *routeOption) bool {
 	return ro.canMerge(inner, func() bool {
 		switch vals := inner.condition.(type) {
@@ -120,7 +114,7 @@ func (ro *routeOption) SubqueryCanMerge(pb *primitiveBuilder, inner *routeOption
 }
 
 func (ro *routeOption) MergeSubquery(subqueryOption *routeOption) {
-	ro.merge(subqueryOption)
+	ro.substitutions = append(ro.substitutions, subqueryOption.substitutions...)
 }
 
 func (ro *routeOption) UnionCanMerge(rro *routeOption) bool {
@@ -128,8 +122,8 @@ func (ro *routeOption) UnionCanMerge(rro *routeOption) bool {
 }
 
 func (ro *routeOption) MergeUnion(rro *routeOption) {
-	ro.merge(rro)
 	ro.vschemaTable = nil
+	ro.substitutions = append(ro.substitutions, rro.substitutions...)
 }
 
 func (ro *routeOption) SubqueryToTable(rb *route, vindexMap map[*column]vindexes.SingleColumn) {
@@ -155,7 +149,9 @@ func (ro *routeOption) canMerge(rro *routeOption, customCheck func() bool) bool
 			return true
 		}
 	case engine.SelectReference:
-		return true
+		// TODO(sougou): this can be changed to true, but we'll have
+		// to merge against rro insteal of ro.
+		return false
 	case engine.SelectNext:
 		return false
 	}


Test patch:
diff --git a/.github/workflows/e2e_race.yml b/.github/workflows/e2e_race.yml
index 040639d96b..0439d06711 100644
--- a/.github/workflows/e2e_race.yml
+++ b/.github/workflows/e2e_race.yml
@@ -3,7 +3,7 @@ on: [push, pull_request]
 jobs:
 
   build:
-    name: End-to-End Test (Race)
+    name: Build
     runs-on: ubuntu-latest
     steps:
 
@@ -17,7 +17,6 @@ jobs:
 
     - name: Get dependencies
       run: |
-        sudo apt-get update
         sudo apt-get install -y mysql-server mysql-client make unzip g++ etcd curl git wget
         sudo service mysql stop
         sudo service etcd stop
diff --git a/go/vt/sqlparser/analyzer_test.go b/go/vt/sqlparser/analyzer_test.go
index f97f36ccbc..8ecc3ce0db 100644
--- a/go/vt/sqlparser/analyzer_test.go
+++ b/go/vt/sqlparser/analyzer_test.go
@@ -50,9 +50,6 @@ func TestPreview(t *testing.T) {
 		{"begin ...", StmtUnknown},
 		{"begin /* ... */", StmtBegin},
 		{"begin /* ... *//*test*/", StmtBegin},
-		{"begin;", StmtBegin},
-		{"begin ;", StmtBegin},
-		{"begin; /*...*/", StmtBegin},
 		{"start transaction", StmtBegin},
 		{"commit", StmtCommit},
 		{"commit /*...*/", StmtCommit},
diff --git a/go/vt/sqlparser/parse_test.go b/go/vt/sqlparser/parse_test.go
index 8ae27a8594..564edccefc 100644
--- a/go/vt/sqlparser/parse_test.go
+++ b/go/vt/sqlparser/parse_test.go
@@ -1442,9 +1442,6 @@ var (
 		input: "stream /* comment */ * from t",
 	}, {
 		input: "begin",
-	}, {
-		input:  "begin;",
-		output: "begin",
 	}, {
 		input:  "start transaction",
 		output: "begin",
diff --git a/go/vt/vtgate/engine/fake_primitive_test.go b/go/vt/vtgate/engine/fake_primitive_test.go
index 2c0641a979..2d00e3e735 100644
--- a/go/vt/vtgate/engine/fake_primitive_test.go
+++ b/go/vt/vtgate/engine/fake_primitive_test.go
@@ -40,12 +40,6 @@ type fakePrimitive struct {
 	log []string
 }
 
-func (f *fakePrimitive) Inputs() []Primitive {
-	return []Primitive{}
-}
-
-var _ Primitive = (*fakePrimitive)(nil)
-
 func (f *fakePrimitive) rewind() {
 	f.curResult = 0
 	f.log = nil
diff --git a/go/vt/vtgate/executor_scatter_stats_test.go b/go/vt/vtgate/executor_scatter_stats_test.go
deleted file mode 100644
index 333a06be91..0000000000
--- a/go/vt/vtgate/executor_scatter_stats_test.go
+++ /dev/null
@@ -1,78 +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 vtgate
-
-import (
-	"context"
-	"testing"
-
-	"net/http/httptest"
-
-	"github.com/stretchr/testify/assert"
-
-	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
-)
-
-func TestScatterStatsWithNoScatterQuery(t *testing.T) {
-	executor, _, _, _ := createExecutorEnv()
-	session := NewSafeSession(&vtgatepb.Session{TargetString: "@master"})
-
-	_, err := executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, "select * from main1", nil)
-	assert.NoError(t, err)
-
-	result, err := executor.gatherScatterStats()
-	assert.NoError(t, err)
-	assert.Equal(t, 0, len(result.Items))
-}
-
-func TestScatterStatsWithSingleScatterQuery(t *testing.T) {
-	executor, _, _, _ := createExecutorEnv()
-	session := NewSafeSession(&vtgatepb.Session{TargetString: "@master"})
-
-	_, err := executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, "select * from user", nil)
-	assert.NoError(t, err)
-
-	result, err := executor.gatherScatterStats()
-	assert.NoError(t, err)
-	assert.Equal(t, 1, len(result.Items))
-}
-
-func TestScatterStatsHttpWriting(t *testing.T) {
-	executor, _, _, _ := createExecutorEnv()
-	session := NewSafeSession(&vtgatepb.Session{TargetString: "@master"})
-
-	_, err := executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, "select * from user", nil)
-	assert.NoError(t, err)
-
-	_, err = executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, "select * from user where Id = 15", nil)
-	assert.NoError(t, err)
-
-	_, err = executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, "select * from user where Id > 15", nil)
-	assert.NoError(t, err)
-
-	query4 := "select * from user as u1 join  user as u2 on u1.Id = u2.Id"
-	_, err = executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, query4, nil)
-	assert.NoError(t, err)
-
-	recorder := httptest.NewRecorder()
-	executor.WriteScatterStats(recorder)
-
-	// Here we are checking that the template was executed correctly.
-	// If it wasn't, instead of html, we'll get an error message
-	assert.Contains(t, recorder.Body.String(), query4)
-	assert.NoError(t, err)
-}
diff --git a/go/vt/vtgate/executor_test.go b/go/vt/vtgate/executor_test.go
index 8767cd0928..8a42e884bc 100644
--- a/go/vt/vtgate/executor_test.go
+++ b/go/vt/vtgate/executor_test.go
@@ -890,6 +890,13 @@ func TestExecutorShow(t *testing.T) {
 	if err != nil {
 		t.Error(err)
 	}
+
+	// Test SHOW FULL COLUMNS FROM where query has a qualifier
+	_, err = executor.Execute(context.Background(), "TestExecute", session, fmt.Sprintf("show full columns from %v.table1", KsTestUnsharded), nil)
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
 	// Just test for first & last.
 	qr.Rows = [][]sqltypes.Value{qr.Rows[0], qr.Rows[len(qr.Rows)-1]}
 	wantqr = &sqltypes.Result{
diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
index 53fae69ba0..0130fe08b7 100644
--- a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
@@ -233,17 +233,13 @@
 {
   "Original": "select user_extra.id from user left join user_extra on user.id = user_extra.user_id where user_extra.user_id = 5",
   "Instructions": {
-    "Opcode": "SelectEqualUnique",
+    "Opcode": "SelectScatter",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
     "Query": "select user_extra.id from user left join user_extra on user.id = user_extra.user_id where user_extra.user_id = 5",
     "FieldQuery": "select user_extra.id from user left join user_extra on user.id = user_extra.user_id where 1 != 1",
-    "Vindex": "user_index",
-    "Values": [
-      5
-    ],
     "Table": "user"
   }
 }
diff --git a/go/vt/vtgate/planbuilder/testdata/from_cases.txt b/go/vt/vtgate/planbuilder/testdata/from_cases.txt
index 67943ac9df..6428be6a65 100644
--- a/go/vt/vtgate/planbuilder/testdata/from_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/from_cases.txt
@@ -927,19 +927,35 @@
   }
 }
 
-# reference table can merge with other opcodes left to right.
+# reference table doesn't merge with other opcodes yet.
 "select ref.col from ref join user"
 {
   "Original": "select ref.col from ref join user",
   "Instructions": {
-    "Opcode": "SelectScatter",
-    "Keyspace": {
-      "Name": "user",
-      "Sharded": true
+    "Opcode": "Join",
+    "Left": {
+      "Opcode": "SelectReference",
+      "Keyspace": {
+        "Name": "user",
+        "Sharded": true
+      },
+      "Query": "select ref.col from ref",
+      "FieldQuery": "select ref.col from ref where 1 != 1",
+      "Table": "ref"
     },
-    "Query": "select ref.col from ref join user",
-    "FieldQuery": "select ref.col from ref join user where 1 != 1",
-    "Table": "user"
+    "Right": {
+      "Opcode": "SelectScatter",
+      "Keyspace": {
+        "Name": "user",
+        "Sharded": true
+      },
+      "Query": "select 1 from user",
+      "FieldQuery": "select 1 from user where 1 != 1",
+      "Table": "user"
+    },
+    "Cols": [
+      -1
+    ]
   }
 }
 
diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
index 67a05f8aee..77cee7da74 100644
--- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
@@ -432,3 +432,8 @@
 
 "select func(keyspace_id) from user_index where id = :id"
 "unsupported: expression on results of a vindex function"
+
+# Multi-table unique vindex constraint on left table of left join, two levels of join, simple aggregation
+# This should work, but doesn't. See https://github.com/vitessio/vitess/issues/4772
+"select user.id, count(*) from user left join user_extra ue1 on user.id = ue1.user_id left join user_extra ue2 on ue1.user_id = ue2.user_id group by user.id"
+"unsupported: cross-shard query with aggregates"