Repository: vitessio/vitess
Pull request #7029 (2020-11-12 12:10:09)

UNION DISTINCT support on vtgate
Includes #7016 

harshit-gangal: Tried on vitess cluster and found panic on the query

`select 1 union select null union select 1.0 union select "1" union select 2 union select 2.0 from product;`
```
E1113 15:04:29.040949  132267 server.go:281] mysql_server caught panic:
interface conversion: sqlparser.SelectStatement is *sqlparser.Union, not *sqlparser.Select
/usr/local/go/src/runtime/iface.go:255 (0x40b983)
	panicdottypeE: panic(&TypeAssertionError{iface, have, want, ""})
/usr/local/go/src/runtime/iface.go:265 (0x40b8c3)
	panicdottypeI: panicdottypeE(t, want, iface)
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/route.go:166 (0xbff14d)
	io/vitess/go/vt/vtgate/planbuilder.(*route).MakeDistinct: rb.Select.(*sqlparser.Select).Distinct = true
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/union.go:58 (0xc0f0ce)
	io/vitess/go/vt/vtgate/planbuilder.(*primitiveBuilder).processUnion: bldr, err := pb.bldr.MakeDistinct()
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/union.go:36 (0xc0ea75)
	io/vitess/go/vt/vtgate/planbuilder.buildUnionPlan: if err := pb.processUnion(union, nil); err != nil {
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/builder.go:302 (0xbe68a4)
	io/vitess/go/vt/vtgate/planbuilder.buildRoutePlan: return f(stmt, vschema)
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/builder.go:316 (0xbe75f1)
	io/vitess/go/vt/vtgate/planbuilder.createInstructionFor: return buildRoutePlan(stmt, vschema, buildUnionPlan)
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/builder.go:329 (0xbe7190)
	io/vitess/go/vt/vtgate/planbuilder.createInstructionFor: innerInstruction, err := createInstructionFor(query, stmt.Statement, vschema)
/home/harshit/git-repo/vitess/go/vt/vtgate/planbuilder/builder.go:285 (0xbe668f)
	io/vitess/go/vt/vtgate/planbuilder.BuildFromStmt: instruction, err := createInstructionFor(query, stmt, vschema)
/home/harshit/git-repo/vitess/go/vt/vtgate/executor.go:1380 (0xc66842)
	io/vitess/go/vt/vtgate.(*Executor).getPlan: plan, err := planbuilder.BuildFromStmt(query, statement, vcursor, bindVarNeeds)
/home/harshit/git-repo/vitess/go/vt/vtgate/plan_execute.go:54 (0xc6d213)
	io/vitess/go/vt/vtgate.(*Executor).newExecute: plan, err := e.getPlan(
/home/harshit/git-repo/vitess/go/vt/vtgate/executor.go:185 (0xc5a13a)
	io/vitess/go/vt/vtgate.(*Executor).execute: stmtType, qr, err := e.newExecute(ctx, safeSession, sql, bindVars, logStats)
/home/harshit/git-repo/vitess/go/vt/vtgate/executor.go:154 (0xc59e57)
	io/vitess/go/vt/vtgate.(*Executor).Execute: stmtType, result, err := e.execute(ctx, safeSession, sql, bindVars, logStats)
/home/harshit/git-repo/vitess/go/vt/vtgate/vtgate.go:261 (0xc8a4d1)
	io/vitess/go/vt/vtgate.(*VTGate).Execute: qr, err = vtg.executor.Execute(ctx, "Execute", NewSafeSession(session), sql, bindVariables)
/home/harshit/git-repo/vitess/go/vt/vtgate/plugin_mysql_server.go:217 (0xc6f3b1)
	io/vitess/go/vt/vtgate.(*vtgateHandler).ComQuery: session, result, err := vh.vtg.Execute(ctx, session, query, make(map[string]*querypb.BindVariable))
/home/harshit/git-repo/vitess/go/mysql/conn.go:1232 (0x8b9b16)
	io/vitess/go/mysql.(*Conn).execQuery: err := handler.ComQuery(c, query, func(qr *sqltypes.Result) error {
/home/harshit/git-repo/vitess/go/mysql/conn.go:1217 (0x8b9688)
	io/vitess/go/mysql.(*Conn).handleComQuery: res := c.execQuery(sql, handler, more)
/home/harshit/git-repo/vitess/go/mysql/conn.go:865 (0x8b686f)
	io/vitess/go/mysql.(*Conn).handleNextCommand: return c.handleComQuery(handler, data)
/home/harshit/git-repo/vitess/go/mysql/server.go:473 (0x8d8e8e)
	io/vitess/go/mysql.(*Listener).handle: kontinue := c.handleNextCommand(l.handler)
/usr/local/go/src/runtime/asm_amd64.s:1357 (0x4613a0)
	goexit: BYTE	$0x90	// NOP
```

Patch:
diff --git a/go/vt/vtgate/engine/distinct.go b/go/vt/vtgate/engine/distinct.go
new file mode 100644
index 0000000000..afd6f2ccb5
--- /dev/null
+++ b/go/vt/vtgate/engine/distinct.go
@@ -0,0 +1,178 @@
+/*
+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 engine
+
+import (
+	"vitess.io/vitess/go/sqltypes"
+	querypb "vitess.io/vitess/go/vt/proto/query"
+	"vitess.io/vitess/go/vt/vtgate/evalengine"
+)
+
+// Distinct Primitive is used to uniqueify results
+var _ Primitive = (*Distinct)(nil)
+
+// Distinct Primitive is used to uniqueify results
+type Distinct struct {
+	Source Primitive
+}
+
+type row = []sqltypes.Value
+
+type probeTable struct {
+	m map[int64][]row
+}
+
+func (pt *probeTable) exists(inputRow row) (bool, error) {
+	// calculate hashcode from all column values in the input row
+	code := int64(17)
+	for _, value := range inputRow {
+		hashcode, err := evalengine.NullsafeHashcode(value)
+		if err != nil {
+			return false, err
+		}
+		code = code*31 + hashcode
+	}
+
+	existingRows, found := pt.m[code]
+	if !found {
+		// nothing with this hash code found, we can be sure it's a not seen row
+		pt.m[code] = []row{inputRow}
+		return false, nil
+	}
+
+	// we found something in the map - still need to check all individual values
+	// so we don't just fall for a hash collision
+	for _, existingRow := range existingRows {
+		exists, err := equal(existingRow, inputRow)
+		if err != nil {
+			return false, err
+		}
+		if exists {
+			return true, nil
+		}
+	}
+
+	pt.m[code] = append(existingRows, inputRow)
+
+	return false, nil
+}
+
+func equal(a, b []sqltypes.Value) (bool, error) {
+	for i, aVal := range a {
+		cmp, err := evalengine.NullsafeCompare(aVal, b[i])
+		if err != nil {
+			return false, err
+		}
+		if cmp != 0 {
+			return false, nil
+		}
+	}
+	return true, nil
+}
+
+func newProbeTable() *probeTable {
+	return &probeTable{m: map[int64][]row{}}
+}
+
+// Execute implements the Primitive interface
+func (d *Distinct) Execute(vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool) (*sqltypes.Result, error) {
+	input, err := d.Source.Execute(vcursor, bindVars, wantfields)
+	if err != nil {
+		return nil, err
+	}
+
+	result := &sqltypes.Result{
+		Fields:   input.Fields,
+		InsertID: input.InsertID,
+	}
+
+	pt := newProbeTable()
+
+	for _, row := range input.Rows {
+		exists, err := pt.exists(row)
+		if err != nil {
+			return nil, err
+		}
+		if !exists {
+			result.Rows = append(result.Rows, row)
+		}
+	}
+
+	result.RowsAffected = uint64(len(result.Rows))
+
+	return result, err
+}
+
+// StreamExecute implements the Primitive interface
+func (d *Distinct) StreamExecute(vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error {
+	pt := newProbeTable()
+
+	err := d.Source.StreamExecute(vcursor, bindVars, wantfields, func(input *sqltypes.Result) error {
+		result := &sqltypes.Result{
+			Fields:   input.Fields,
+			InsertID: input.InsertID,
+		}
+		for _, row := range input.Rows {
+			exists, err := pt.exists(row)
+			if err != nil {
+				return err
+			}
+			if !exists {
+				result.Rows = append(result.Rows, row)
+			}
+		}
+		return callback(result)
+	})
+
+	return err
+}
+
+// RouteType implements the Primitive interface
+func (d *Distinct) RouteType() string {
+	return d.Source.RouteType()
+}
+
+// GetKeyspaceName implements the Primitive interface
+func (d *Distinct) GetKeyspaceName() string {
+	return d.Source.GetKeyspaceName()
+}
+
+// GetTableName implements the Primitive interface
+func (d *Distinct) GetTableName() string {
+	return d.Source.GetTableName()
+}
+
+// GetFields implements the Primitive interface
+func (d *Distinct) GetFields(vcursor VCursor, bindVars map[string]*querypb.BindVariable) (*sqltypes.Result, error) {
+	return d.Source.GetFields(vcursor, bindVars)
+}
+
+// NeedsTransaction implements the Primitive interface
+func (d *Distinct) NeedsTransaction() bool {
+	return d.Source.NeedsTransaction()
+}
+
+// Inputs implements the Primitive interface
+func (d *Distinct) Inputs() []Primitive {
+	return []Primitive{d.Source}
+}
+
+func (d *Distinct) description() PrimitiveDescription {
+	return PrimitiveDescription{
+		OperatorType: "Distinct",
+	}
+}
diff --git a/go/vt/vtgate/engine/route.go b/go/vt/vtgate/engine/route.go
index 1c69551f9a..7d8fe4dc30 100644
--- a/go/vt/vtgate/engine/route.go
+++ b/go/vt/vtgate/engine/route.go
@@ -690,6 +690,10 @@ func (route *Route) description() PrimitiveDescription {
 	if route.SysTableTableName != nil {
 		other["SysTableTableName"] = route.SysTableTableName.String()
 	}
+	orderBy := GenericJoin(route.OrderBy, orderByToString)
+	if orderBy != "" {
+		other["OrderBy"] = orderBy
+	}
 
 	return PrimitiveDescription{
 		OperatorType:      "Route",
@@ -700,5 +704,9 @@ func (route *Route) description() PrimitiveDescription {
 	}
 }
 
+func orderByToString(in interface{}) string {
+	return in.(OrderbyParams).String()
+}
+
 // BvTableName is used to fill in the table name for information_schema queries with routed tables
 const BvTableName = "__vttablename"
diff --git a/go/vt/vtgate/planbuilder/builder.go b/go/vt/vtgate/planbuilder/builder.go
index 3109f5dcdf..2ec54699ce 100644
--- a/go/vt/vtgate/planbuilder/builder.go
+++ b/go/vt/vtgate/planbuilder/builder.go
@@ -68,7 +68,7 @@ type builder interface {
 	PushSelect(pb *primitiveBuilder, expr *sqlparser.AliasedExpr, origin builder) (rc *resultColumn, colNumber int, err error)
 
 	// MakeDistinct makes the primitive handle the distinct clause.
-	MakeDistinct() error
+	MakeDistinct() (builder, error)
 	// PushGroupBy makes the primitive handle the GROUP BY clause.
 	PushGroupBy(sqlparser.GroupBy) error
 
diff --git a/go/vt/vtgate/planbuilder/concatenate.go b/go/vt/vtgate/planbuilder/concatenate.go
index 72a359445a..1956ec7be7 100644
--- a/go/vt/vtgate/planbuilder/concatenate.go
+++ b/go/vt/vtgate/planbuilder/concatenate.go
@@ -89,8 +89,8 @@ func (c *concatenate) PushSelect(pb *primitiveBuilder, expr *sqlparser.AliasedEx
 	return nil, 0, unreachable("Select")
 }
 
-func (c *concatenate) MakeDistinct() error {
-	return vterrors.New(vtrpc.Code_UNIMPLEMENTED, "only union-all is supported for this operator")
+func (c *concatenate) MakeDistinct() (builder, error) {
+	return newDistinct(c), nil
 }
 
 func (c *concatenate) PushGroupBy(by sqlparser.GroupBy) error {
diff --git a/go/vt/vtgate/planbuilder/distinct.go b/go/vt/vtgate/planbuilder/distinct.go
new file mode 100644
index 0000000000..56381c411d
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/distinct.go
@@ -0,0 +1,73 @@
+/*
+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 planbuilder
+
+import (
+	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vtgate/engine"
+)
+
+var _ builder = (*distinct)(nil)
+
+// limit is the builder for engine.Limit.
+// This gets built if a limit needs to be applied
+// after rows are returned from an underlying
+// operation. Since a limit is the final operation
+// of a SELECT, most pushes are not applicable.
+type distinct struct {
+	builderCommon
+}
+
+func newDistinct(source builder) builder {
+	return &distinct{
+		builderCommon: newBuilderCommon(source),
+	}
+}
+
+func (d *distinct) PushFilter(pb *primitiveBuilder, filter sqlparser.Expr, whereType string, origin builder) error {
+	return d.input.PushFilter(pb, filter, whereType, origin)
+}
+
+func (d *distinct) PushSelect(pb *primitiveBuilder, expr *sqlparser.AliasedExpr, origin builder) (rc *resultColumn, colNumber int, err error) {
+	return d.input.PushSelect(pb, expr, origin)
+}
+
+func (d *distinct) MakeDistinct() (builder, error) {
+	return d, nil
+}
+
+func (d *distinct) PushGroupBy(by sqlparser.GroupBy) error {
+	return d.input.PushGroupBy(by)
+}
+
+func (d *distinct) PushOrderBy(by sqlparser.OrderBy) (builder, error) {
+	orderBy, err := d.input.PushOrderBy(by)
+	if err != nil {
+		return nil, err
+	}
+	return &distinct{builderCommon: newBuilderCommon(orderBy)}, nil
+}
+
+func (d *distinct) PushLock(lock sqlparser.Lock) error {
+	return d.input.PushLock(lock)
+}
+
+func (d *distinct) Primitive() engine.Primitive {
+	return &engine.Distinct{
+		Source: d.input.Primitive(),
+	}
+}
diff --git a/go/vt/vtgate/planbuilder/from.go b/go/vt/vtgate/planbuilder/from.go
index 57cd4dac6a..5cbe734e6c 100644
--- a/go/vt/vtgate/planbuilder/from.go
+++ b/go/vt/vtgate/planbuilder/from.go
@@ -20,6 +20,9 @@ import (
 	"errors"
 	"fmt"
 
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"vitess.io/vitess/go/vt/vterrors"
+
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
@@ -71,7 +74,11 @@ func (pb *primitiveBuilder) processTableExpr(tableExpr sqlparser.TableExpr) erro
 		// don't associate differently when more things are pushed
 		// into it. FROM a, (b, c) should not become FROM a, b, c.
 		if rb, ok := pb.bldr.(*route); ok {
-			sel := rb.Select.(*sqlparser.Select)
+			sel, ok := rb.Select.(*sqlparser.Select)
+			if !ok {
+				return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query: %s", sqlparser.String(rb.Select))
+			}
+
 			sel.From = sqlparser.TableExprs{&sqlparser.ParenTableExpr{Exprs: sel.From}}
 		}
 		return err
@@ -323,9 +330,15 @@ func (pb *primitiveBuilder) join(rpb *primitiveBuilder, ajoin *sqlparser.JoinTab
 	rRoute.Redirect = lRoute
 
 	// Merge the AST.
-	sel := lRoute.Select.(*sqlparser.Select)
+	sel, ok := lRoute.Select.(*sqlparser.Select)
+	if !ok {
+		return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query: %s", sqlparser.String(lRoute.Select))
+	}
 	if ajoin == nil {
-		rhsSel := rRoute.Select.(*sqlparser.Select)
+		rhsSel, ok := rRoute.Select.(*sqlparser.Select)
+		if !ok {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query: %s", sqlparser.String(rRoute.Select))
+		}
 		sel.From = append(sel.From, rhsSel.From...)
 	} else {
 		sel.From = sqlparser.TableExprs{ajoin}
diff --git a/go/vt/vtgate/planbuilder/join.go b/go/vt/vtgate/planbuilder/join.go
index c33005bfbf..1c61ba3f35 100644
--- a/go/vt/vtgate/planbuilder/join.go
+++ b/go/vt/vtgate/planbuilder/join.go
@@ -190,8 +190,8 @@ func (jb *join) PushSelect(pb *primitiveBuilder, expr *sqlparser.AliasedExpr, or
 }
 
 // MakeDistinct satisfies the builder interface.
-func (jb *join) MakeDistinct() error {
-	return errors.New("unsupported: distinct on cross-shard join")
+func (jb *join) MakeDistinct() (builder, error) {
+	return nil, errors.New("unsupported: distinct on cross-shard join")
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/limit.go b/go/vt/vtgate/planbuilder/limit.go
index 22d6836976..bde79f14f3 100644
--- a/go/vt/vtgate/planbuilder/limit.go
+++ b/go/vt/vtgate/planbuilder/limit.go
@@ -66,8 +66,8 @@ func (l *limit) PushSelect(_ *primitiveBuilder, expr *sqlparser.AliasedExpr, ori
 }
 
 // MakeDistinct satisfies the builder interface.
-func (l *limit) MakeDistinct() error {
-	return errors.New("limit.MakeDistinct: unreachable")
+func (l *limit) MakeDistinct() (builder, error) {
+	return nil, errors.New("limit.MakeDistinct: unreachable")
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/memory_sort.go b/go/vt/vtgate/planbuilder/memory_sort.go
index 306f547db4..c93d06101b 100644
--- a/go/vt/vtgate/planbuilder/memory_sort.go
+++ b/go/vt/vtgate/planbuilder/memory_sort.go
@@ -99,8 +99,8 @@ func (ms *memorySort) PushSelect(_ *primitiveBuilder, expr *sqlparser.AliasedExp
 }
 
 // MakeDistinct satisfies the builder interface.
-func (ms *memorySort) MakeDistinct() error {
-	return errors.New("memorySort.MakeDistinct: unreachable")
+func (ms *memorySort) MakeDistinct() (builder, error) {
+	return nil, errors.New("memorySort.MakeDistinct: unreachable")
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/merge_sort.go b/go/vt/vtgate/planbuilder/merge_sort.go
index 273fef2f76..6ee2058270 100644
--- a/go/vt/vtgate/planbuilder/merge_sort.go
+++ b/go/vt/vtgate/planbuilder/merge_sort.go
@@ -76,8 +76,13 @@ func (ms *mergeSort) PushSelect(pb *primitiveBuilder, expr *sqlparser.AliasedExp
 }
 
 // MakeDistinct satisfies the builder interface.
-func (ms *mergeSort) MakeDistinct() error {
-	return ms.input.MakeDistinct()
+func (ms *mergeSort) MakeDistinct() (builder, error) {
+	distinctSrc, err := ms.input.MakeDistinct()
+	if err != nil {
+		return nil, err
+	}
+	ms.input = distinctSrc
+	return ms, err
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/ordered_aggregate.go b/go/vt/vtgate/planbuilder/ordered_aggregate.go
index ba1744afea..e7cf2bc05d 100644
--- a/go/vt/vtgate/planbuilder/ordered_aggregate.go
+++ b/go/vt/vtgate/planbuilder/ordered_aggregate.go
@@ -356,18 +356,23 @@ func (oa *orderedAggregate) needDistinctHandling(pb *primitiveBuilder, funcExpr
 	return true, innerAliased, nil
 }
 
-func (oa *orderedAggregate) MakeDistinct() error {
+func (oa *orderedAggregate) MakeDistinct() (builder, error) {
 	for i, rc := range oa.resultColumns {
 		// If the column origin is oa (and not the underlying route),
 		// it means that it's an aggregate function supplied by oa.
 		// So, the distinct 'operator' cannot be pushed down into the
 		// route.
 		if rc.column.Origin() == oa {
-			return errors.New("unsupported: distinct cannot be combined with aggregate functions")
+			return nil, errors.New("unsupported: distinct cannot be combined with aggregate functions")
 		}
 		oa.eaggr.Keys = append(oa.eaggr.Keys, i)
 	}
-	return oa.input.MakeDistinct()
+	distinctSrc, err := oa.input.MakeDistinct()
+	if err != nil {
+		return nil, err
+	}
+	oa.input = distinctSrc
+	return oa, err
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/postprocess.go b/go/vt/vtgate/planbuilder/postprocess.go
index 33e16ad54f..0ec85a1575 100644
--- a/go/vt/vtgate/planbuilder/postprocess.go
+++ b/go/vt/vtgate/planbuilder/postprocess.go
@@ -27,9 +27,11 @@ import (
 // and ensures that there are no subqueries.
 func (pb *primitiveBuilder) pushGroupBy(sel *sqlparser.Select) error {
 	if sel.Distinct {
-		if err := pb.bldr.MakeDistinct(); err != nil {
+		newBuilder, err := pb.bldr.MakeDistinct()
+		if err != nil {
 			return err
 		}
+		pb.bldr = newBuilder
 	}
 
 	if err := pb.st.ResolveSymbols(sel.GroupBy); err != nil {
diff --git a/go/vt/vtgate/planbuilder/pullout_subquery.go b/go/vt/vtgate/planbuilder/pullout_subquery.go
index 34ec7ad8eb..36a695563d 100644
--- a/go/vt/vtgate/planbuilder/pullout_subquery.go
+++ b/go/vt/vtgate/planbuilder/pullout_subquery.go
@@ -102,8 +102,13 @@ func (ps *pulloutSubquery) PushSelect(pb *primitiveBuilder, expr *sqlparser.Alia
 }
 
 // MakeDistinct satisfies the builder interface.
-func (ps *pulloutSubquery) MakeDistinct() error {
-	return ps.underlying.MakeDistinct()
+func (ps *pulloutSubquery) MakeDistinct() (builder, error) {
+	distinctUnderlying, err := ps.underlying.MakeDistinct()
+	if err != nil {
+		return nil, err
+	}
+	ps.underlying = distinctUnderlying
+	return ps, err
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/route.go b/go/vt/vtgate/planbuilder/route.go
index 8ae783edd9..e58599cb2d 100644
--- a/go/vt/vtgate/planbuilder/route.go
+++ b/go/vt/vtgate/planbuilder/route.go
@@ -124,7 +124,10 @@ func (rb *route) ResultColumns() []*resultColumn {
 // PushFilter satisfies the builder interface.
 // The primitive will be updated if the new filter improves the plan.
 func (rb *route) PushFilter(pb *primitiveBuilder, filter sqlparser.Expr, whereType string, _ builder) error {
-	sel := rb.Select.(*sqlparser.Select)
+	sel, ok := rb.Select.(*sqlparser.Select)
+	if !ok {
+		return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query")
+	}
 	switch whereType {
 	case sqlparser.WhereStr:
 		sel.AddWhere(filter)
@@ -137,7 +140,11 @@ func (rb *route) PushFilter(pb *primitiveBuilder, filter sqlparser.Expr, whereTy
 
 // PushSelect satisfies the builder interface.
 func (rb *route) PushSelect(_ *primitiveBuilder, expr *sqlparser.AliasedExpr, _ builder) (rc *resultColumn, colNumber int, err error) {
-	sel := rb.Select.(*sqlparser.Select)
+	sel, ok := rb.Select.(*sqlparser.Select)
+	if !ok {
+		return nil, 0, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query")
+	}
+
 	sel.SelectExprs = append(sel.SelectExprs, expr)
 
 	rc = newResultColumn(expr, rb)
@@ -150,6 +157,7 @@ func (rb *route) PushSelect(_ *primitiveBuilder, expr *sqlparser.AliasedExpr, _
 // into the select expression list of the route. This function is
 // similar to PushSelect.
 func (rb *route) PushAnonymous(expr sqlparser.SelectExpr) *resultColumn {
+	// TODO: we should not assume that the query is a SELECT
 	sel := rb.Select.(*sqlparser.Select)
 	sel.SelectExprs = append(sel.SelectExprs, expr)
 
@@ -162,14 +170,22 @@ func (rb *route) PushAnonymous(expr sqlparser.SelectExpr) *resultColumn {
 }
 
 // MakeDistinct satisfies the builder interface.
-func (rb *route) MakeDistinct() error {
-	rb.Select.(*sqlparser.Select).Distinct = true
-	return nil
+func (rb *route) MakeDistinct() (builder, error) {
+	s, ok := rb.Select.(*sqlparser.Select)
+	if !ok {
+		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query")
+	}
+	s.Distinct = true
+	return rb, nil
 }
 
 // PushGroupBy satisfies the builder interface.
 func (rb *route) PushGroupBy(groupBy sqlparser.GroupBy) error {
-	rb.Select.(*sqlparser.Select).GroupBy = groupBy
+	s, ok := rb.Select.(*sqlparser.Select)
+	if !ok {
+		return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query")
+	}
+	s.GroupBy = groupBy
 	return nil
 }
 
@@ -252,13 +268,17 @@ func (rb *route) SetUpperLimit(count sqlparser.Expr) {
 
 // PushMisc satisfies the builder interface.
 func (rb *route) PushMisc(sel *sqlparser.Select) error {
-	rb.Select.(*sqlparser.Select).Comments = sel.Comments
-	rb.Select.(*sqlparser.Select).Lock = sel.Lock
+	s, ok := rb.Select.(*sqlparser.Select)
+	if !ok {
+		return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query")
+	}
+	s.Comments = sel.Comments
+	s.Lock = sel.Lock
 	if sel.Into != nil {
 		if rb.eroute.Opcode != engine.SelectUnsharded {
 			return vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: this construct is not supported on sharded keyspace")
 		}
-		rb.Select.(*sqlparser.Select).Into = sel.Into
+		s.Into = sel.Into
 	}
 	return nil
 }
@@ -420,6 +440,7 @@ func (rb *route) SupplyCol(col *sqlparser.ColName) (rc *resultColumn, colNumber
 	// A new result has to be returned.
 	rc = &resultColumn{column: c}
 	rb.resultColumns = append(rb.resultColumns, rc)
+	// TODO: we should not assume that the query is a SELECT query
 	sel := rb.Select.(*sqlparser.Select)
 	sel.SelectExprs = append(sel.SelectExprs, &sqlparser.AliasedExpr{Expr: col})
 	return rc, len(rb.resultColumns) - 1
@@ -431,11 +452,16 @@ func (rb *route) SupplyWeightString(colNumber int) (weightcolNumber int, err err
 	if weightcolNumber, ok := rb.weightStrings[rc]; ok {
 		return weightcolNumber, nil
 	}
+	s, ok := rb.Select.(*sqlparser.Select)
+	if !ok {
+		return 0, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected AST struct for query")
+	}
+
 	expr := &sqlparser.AliasedExpr{
 		Expr: &sqlparser.FuncExpr{
 			Name: sqlparser.NewColIdent("weight_string"),
 			Exprs: []sqlparser.SelectExpr{
-				rb.Select.(*sqlparser.Select).SelectExprs[colNumber],
+				s.SelectExprs[colNumber],
 			},
 		},
 	}
diff --git a/go/vt/vtgate/planbuilder/sql_calc_found_rows.go b/go/vt/vtgate/planbuilder/sql_calc_found_rows.go
index 63c55113f0..0aa0d81501 100644
--- a/go/vt/vtgate/planbuilder/sql_calc_found_rows.go
+++ b/go/vt/vtgate/planbuilder/sql_calc_found_rows.go
@@ -80,8 +80,8 @@ func (s *sqlCalcFoundRows) PushSelect(*primitiveBuilder, *sqlparser.AliasedExpr,
 }
 
 //MakeDistinct implements the builder interface
-func (s *sqlCalcFoundRows) MakeDistinct() error {
-	return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unreachable: sqlCalcFoundRows.MakeDistinct")
+func (s *sqlCalcFoundRows) MakeDistinct() (builder, error) {
+	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unreachable: sqlCalcFoundRows.MakeDistinct")
 }
 
 //PushGroupBy implements the builder interface
diff --git a/go/vt/vtgate/planbuilder/subquery.go b/go/vt/vtgate/planbuilder/subquery.go
index a6f49faabf..9b0c083daa 100644
--- a/go/vt/vtgate/planbuilder/subquery.go
+++ b/go/vt/vtgate/planbuilder/subquery.go
@@ -113,8 +113,8 @@ func (sq *subquery) PushSelect(_ *primitiveBuilder, expr *sqlparser.AliasedExpr,
 }
 
 // MakeDistinct satisfies the builder interface.
-func (sq *subquery) MakeDistinct() error {
-	return errors.New("unsupported: distinct on cross-shard subquery")
+func (sq *subquery) MakeDistinct() (builder, error) {
+	return nil, errors.New("unsupported: distinct on cross-shard subquery")
 }
 
 // PushGroupBy satisfies the builder interface.
diff --git a/go/vt/vtgate/planbuilder/union.go b/go/vt/vtgate/planbuilder/union.go
index 217d02cafd..728950bb10 100644
--- a/go/vt/vtgate/planbuilder/union.go
+++ b/go/vt/vtgate/planbuilder/union.go
@@ -53,10 +53,6 @@ func (pb *primitiveBuilder) processUnion(union *sqlparser.Union, outer *symtab)
 		}
 		err := unionRouteMerge(pb.bldr, rpb.bldr, us)
 		if err != nil {
-			if us.Distinct {
-				return err
-			}
-
 			// we are merging between two routes - let's check if we can see so that we have the same amount of columns on both sides of the union
 			lhsCols := len(pb.bldr.ResultColumns())
 			rhsCols := len(rpb.bldr.ResultColumns())
@@ -73,6 +69,10 @@ func (pb *primitiveBuilder) processUnion(union *sqlparser.Union, outer *symtab)
 				lhs: pb.bldr,
 				rhs: rpb.bldr,
 			}
+
+			if us.Distinct {
+				pb.bldr = newDistinct(pb.bldr)
+			}
 		}
 		pb.st.Outer = outer
 	}
@@ -132,6 +132,7 @@ func checkOrderByAndLimit(part *sqlparser.Select) error {
 	return nil
 }
 
+// TODO (systay) we never use this as an actual error. we should rethink the return type
 func unionRouteMerge(left, right builder, us *sqlparser.UnionSelect) error {
 	lroute, ok := left.(*route)
 	if !ok {
diff --git a/go/vt/vtgate/planbuilder/vindex_func.go b/go/vt/vtgate/planbuilder/vindex_func.go
index d11e91746d..e255f3a078 100644
--- a/go/vt/vtgate/planbuilder/vindex_func.go
+++ b/go/vt/vtgate/planbuilder/vindex_func.go
@@ -156,8 +156,8 @@ func (vf *vindexFunc) PushSelect(_ *primitiveBuilder, expr *sqlparser.AliasedExp
 }
 
 // MakeDistinct satisfies the builder interface.
-func (vf *vindexFunc) MakeDistinct() error {
-	return errors.New("unsupported: distinct on vindex function")
+func (vf *vindexFunc) MakeDistinct() (builder, error) {
+	return nil, errors.New("unsupported: distinct on vindex function")
 }
 
 // PushGroupBy satisfies the builder interface.


Test patch:
diff --git a/go/test/endtoend/vtgate/misc_test.go b/go/test/endtoend/vtgate/misc_test.go
index 77f3a5dd6d..22bb16d434 100644
--- a/go/test/endtoend/vtgate/misc_test.go
+++ b/go/test/endtoend/vtgate/misc_test.go
@@ -117,9 +117,43 @@ func TestUnionAll(t *testing.T) {
 	exec(t, conn, "insert into t1(id1, id2) values(3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8)")
 
 	// union all between two selectuniquein tables
-	qr := exec(t, conn, "select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8) union all select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8)")
-	expected := utils.SortString("[[INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)] [INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)]]")
-	assert.Equal(t, expected, utils.SortString(fmt.Sprintf("%v", qr.Rows)))
+	assertMatchesNoOrder(t, conn, "select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8) union all select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8)",
+		"[[INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)] [INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)]]")
+}
+
+func TestUnionDistinct(t *testing.T) {
+	conn, err := mysql.Connect(context.Background(), &vtParams)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	// clean up before & after
+	exec(t, conn, "delete from t1")
+	exec(t, conn, "delete from t2")
+	defer func() {
+		exec(t, conn, "set workload = oltp")
+		exec(t, conn, "delete from t1")
+		exec(t, conn, "delete from t2")
+	}()
+
+	exec(t, conn, "insert into t1(id1, id2) values (1, 1), (2, 2), (3,3), (4,4)")
+	exec(t, conn, "insert into t2(id3, id4) values (2, 3), (3, 4), (4,4), (5,5)")
+
+	for _, workload := range []string{"oltp", "olap"} {
+		t.Run(workload, func(t *testing.T) {
+			exec(t, conn, "set workload = "+workload)
+			assertMatches(t, conn, "select 1 union select null", "[[INT64(1)] [NULL]]")
+			assertMatches(t, conn, "select null union select null", "[[NULL]]")
+			assertMatches(t, conn, "select * from (select 1 as col union select 2) as t", "[[INT64(1)] [INT64(2)]]")
+			assertMatches(t, conn, "select 1 from dual where 1 IN (select 1 as col union select 2)", "[[INT64(1)]]")
+
+			// test with real data coming from mysql
+			assertMatches(t, conn, "select id1 from t1 where id1 = 1 union select id1 from t1 where id1 = 5", "[[INT64(1)]]")
+			assertMatchesNoOrder(t, conn, "select id1 from t1 where id1 = 1 union select id1 from t1 where id1 = 4", "[[INT64(1)] [INT64(4)]]")
+			assertMatchesNoOrder(t, conn, "select id1 from t1 where id1 = 1 union select 452 union select id1 from t1 where id1 = 4", "[[INT64(1)] [INT64(452)] [INT64(4)]]")
+			assertMatchesNoOrder(t, conn, "select id1, id2 from t1 union select 827, 452 union select id3,id4 from t2",
+				"[[INT64(4) INT64(4)] [INT64(1) INT64(1)] [INT64(2) INT64(2)] [INT64(3) INT64(3)] [INT64(827) INT64(452)] [INT64(2) INT64(3)] [INT64(3) INT64(4)] [INT64(5) INT64(5)]]")
+		})
+	}
 }
 
 func TestUnionAllOlap(t *testing.T) {
@@ -128,14 +162,18 @@ func TestUnionAllOlap(t *testing.T) {
 	defer conn.Close()
 
 	// clean up before & after
-	defer exec(t, conn, "delete from t1")
-	defer exec(t, conn, "delete from t2")
+	exec(t, conn, "delete from t1")
+	exec(t, conn, "delete from t2")
+	defer func() {
+		exec(t, conn, "set workload = oltp")
+		exec(t, conn, "delete from t1")
+		exec(t, conn, "delete from t2")
+	}()
 
 	exec(t, conn, "insert into t1(id1, id2) values(1, 1), (2, 2)")
 	exec(t, conn, "insert into t2(id3, id4) values(3, 3), (4, 4)")
 
 	exec(t, conn, "set workload = olap")
-	defer exec(t, conn, "set workload = oltp")
 
 	// union all between two selectuniqueequal
 	assertMatches(t, conn, "select id1 from t1 where id1 = 1 union all select id1 from t1 where id1 = 4", "[[INT64(1)]]")
@@ -158,9 +196,9 @@ func TestUnionAllOlap(t *testing.T) {
 	exec(t, conn, "set workload = olap")
 
 	// union all between two selectuniquein tables
-	qr := exec(t, conn, "select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8) union all select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8)")
-	expected := utils.SortString("[[INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)] [INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)]]")
-	assert.Equal(t, expected, utils.SortString(fmt.Sprintf("%v", qr.Rows)))
+	assertMatchesNoOrder(t, conn, "select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8) union all select id1 from t1 where id1 in (1, 2, 3, 4, 5, 6, 7, 8)",
+		"[[INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)] [INT64(1)] [INT64(2)] [INT64(3)] [INT64(5)] [INT64(4)] [INT64(6)] [INT64(7)] [INT64(8)]]")
+
 }
 
 func TestUnion(t *testing.T) {
@@ -403,6 +441,12 @@ func assertMatches(t *testing.T, conn *mysql.Conn, query, expected string) {
 		t.Errorf("Query: %s (-want +got):\n%s", query, diff)
 	}
 }
+func assertMatchesNoOrder(t *testing.T, conn *mysql.Conn, query, expected string) {
+	t.Helper()
+	qr := exec(t, conn, query)
+	actual := fmt.Sprintf("%v", qr.Rows)
+	assert.Equal(t, utils.SortString(expected), utils.SortString(actual), "for query: [%s] expected \n%s \nbut actual \n%s", query, expected, actual)
+}
 
 func assertIsEmpty(t *testing.T, conn *mysql.Conn, query string) {
 	t.Helper()
diff --git a/go/test/utils/sort.go b/go/test/utils/sort.go
index f3584ef70a..2cb6df28a5 100644
--- a/go/test/utils/sort.go
+++ b/go/test/utils/sort.go
@@ -1,3 +1,19 @@
+/*
+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 utils
 
 import (
@@ -5,7 +21,7 @@ import (
 	"strings"
 )
 
-//SortString sorts the string.
+// SortString sorts the string.
 func SortString(w string) string {
 	s := strings.Split(w, "")
 	sort.Strings(s)
diff --git a/go/vt/vtgate/engine/concatenate_test.go b/go/vt/vtgate/engine/concatenate_test.go
index 69e8c9c967..e3128772a0 100644
--- a/go/vt/vtgate/engine/concatenate_test.go
+++ b/go/vt/vtgate/engine/concatenate_test.go
@@ -28,6 +28,10 @@ import (
 	"vitess.io/vitess/go/sqltypes"
 )
 
+func r(names, types string, rows ...string) *sqltypes.Result {
+	return sqltypes.MakeTestResult(sqltypes.MakeTestFields(names, types), rows...)
+}
+
 func TestConcatenate_NoErrors(t *testing.T) {
 	type testCase struct {
 		testName       string
@@ -39,46 +43,46 @@ func TestConcatenate_NoErrors(t *testing.T) {
 	testCases := []*testCase{{
 		testName: "empty results",
 		inputs: []*sqltypes.Result{
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id1|col11|col12", "int64|varbinary|varbinary")),
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id2|col21|col22", "int64|varbinary|varbinary")),
+			r("id1|col11|col12", "int64|varbinary|varbinary"),
+			r("id2|col21|col22", "int64|varbinary|varbinary"),
 		},
-		expectedResult: sqltypes.MakeTestResult(sqltypes.MakeTestFields("id1|col11|col12", "int64|varbinary|varbinary")),
+		expectedResult: r("id1|col11|col12", "int64|varbinary|varbinary"),
 	}, {
 		testName: "2 non empty result",
 		inputs: []*sqltypes.Result{
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("myid|mycol1|mycol2", "int64|varchar|varbinary"), "11|m1|n1", "22|m2|n2"),
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col1|col2", "int64|varchar|varbinary"), "1|a1|b1", "2|a2|b2"),
+			r("myid|mycol1|mycol2", "int64|varchar|varbinary", "11|m1|n1", "22|m2|n2"),
+			r("id|col1|col2", "int64|varchar|varbinary", "1|a1|b1", "2|a2|b2"),
 		},
-		expectedResult: sqltypes.MakeTestResult(sqltypes.MakeTestFields("myid|mycol1|mycol2", "int64|varchar|varbinary"), "11|m1|n1", "22|m2|n2", "1|a1|b1", "2|a2|b2"),
+		expectedResult: r("myid|mycol1|mycol2", "int64|varchar|varbinary", "11|m1|n1", "22|m2|n2", "1|a1|b1", "2|a2|b2"),
 	}, {
 		testName: "mismatch field type",
 		inputs: []*sqltypes.Result{
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col1|col2", "int64|varbinary|varbinary"), "1|a1|b1", "2|a2|b2"),
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col3|col4", "int64|varchar|varbinary"), "1|a1|b1", "2|a2|b2"),
+			r("id|col1|col2", "int64|varbinary|varbinary", "1|a1|b1", "2|a2|b2"),
+			r("id|col3|col4", "int64|varchar|varbinary", "1|a1|b1", "2|a2|b2"),
 		},
 		expectedError: "column field type does not match for name",
 	}, {
 		testName: "input source has different column count",
 		inputs: []*sqltypes.Result{
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col1|col2", "int64|varchar|varchar"), "1|a1|b1", "2|a2|b2"),
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col3|col4|col5", "int64|varchar|varchar|int32"), "1|a1|b1|5", "2|a2|b2|6"),
+			r("id|col1|col2", "int64|varchar|varchar", "1|a1|b1", "2|a2|b2"),
+			r("id|col3|col4|col5", "int64|varchar|varchar|int32", "1|a1|b1|5", "2|a2|b2|6"),
 		},
 		expectedError: "The used SELECT statements have a different number of columns",
 	}, {
 		testName: "1 empty result and 1 non empty result",
 		inputs: []*sqltypes.Result{
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("myid|mycol1|mycol2", "int64|varchar|varbinary")),
-			sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col1|col2", "int64|varchar|varbinary"), "1|a1|b1", "2|a2|b2"),
+			r("myid|mycol1|mycol2", "int64|varchar|varbinary"),
+			r("id|col1|col2", "int64|varchar|varbinary", "1|a1|b1", "2|a2|b2"),
 		},
-		expectedResult: sqltypes.MakeTestResult(sqltypes.MakeTestFields("myid|mycol1|mycol2", "int64|varchar|varbinary"), "1|a1|b1", "2|a2|b2"),
+		expectedResult: r("myid|mycol1|mycol2", "int64|varchar|varbinary", "1|a1|b1", "2|a2|b2"),
 	}}
 
 	for _, tc := range testCases {
 		require.Equal(t, 2, len(tc.inputs))
 		concatenate := &Concatenate{
 			Sources: []Primitive{
-				&fakePrimitive{results: []*sqltypes.Result{tc.inputs[0], tc.inputs[0]}, sendErr: errors.New("abc")},
-				&fakePrimitive{results: []*sqltypes.Result{tc.inputs[1], tc.inputs[1]}, sendErr: errors.New("abc")},
+				&fakePrimitive{results: []*sqltypes.Result{tc.inputs[0], tc.inputs[0]}},
+				&fakePrimitive{results: []*sqltypes.Result{tc.inputs[1], tc.inputs[1]}},
 			},
 		}
 
@@ -110,7 +114,7 @@ func TestConcatenate_WithErrors(t *testing.T) {
 	strFailed := "failed"
 	errString := "Concatenate.Execute: " + strFailed
 
-	fake := sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col1|col2", "int64|varchar|varbinary"), "1|a1|b1", "2|a2|b2")
+	fake := r("id|col1|col2", "int64|varchar|varbinary", "1|a1|b1", "2|a2|b2")
 	concatenate := &Concatenate{
 		Sources: []Primitive{
 			&fakePrimitive{results: []*sqltypes.Result{nil, nil}, sendErr: errors.New(strFailed)},
diff --git a/go/vt/vtgate/engine/distinct_test.go b/go/vt/vtgate/engine/distinct_test.go
new file mode 100644
index 0000000000..daaee343ce
--- /dev/null
+++ b/go/vt/vtgate/engine/distinct_test.go
@@ -0,0 +1,89 @@
+/*
+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 engine
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"vitess.io/vitess/go/test/utils"
+
+	"github.com/stretchr/testify/require"
+	"vitess.io/vitess/go/sqltypes"
+)
+
+func TestDistinct(t *testing.T) {
+	type testCase struct {
+		testName       string
+		inputs         *sqltypes.Result
+		expectedResult *sqltypes.Result
+		expectedError  string
+	}
+
+	testCases := []*testCase{{
+		testName:       "empty",
+		inputs:         r("id1|col11|col12", "int64|varbinary|varbinary"),
+		expectedResult: r("id1|col11|col12", "int64|varbinary|varbinary"),
+	}, {
+		testName:       "int64 numbers",
+		inputs:         r("myid", "int64", "0", "1", "1", "null", "null"),
+		expectedResult: r("myid", "int64", "0", "1", "null"),
+	}, {
+		testName:       "int64 numbers, two columns",
+		inputs:         r("a|b", "int64|int64", "0|0", "1|1", "1|1", "null|null", "null|null", "1|2"),
+		expectedResult: r("a|b", "int64|int64", "0|0", "1|1", "null|null", "1|2"),
+	}, {
+		testName:       "float64 columns designed to produce the same hashcode but not be equal",
+		inputs:         r("a|b", "float64|float64", "0.1|0.2", "0.1|0.3", "0.1|0.4", "0.1|0.5"),
+		expectedResult: r("a|b", "float64|float64", "0.1|0.2", "0.1|0.3", "0.1|0.4", "0.1|0.5"),
+	}, {
+		testName:      "varchar columns",
+		inputs:        r("myid", "varchar", "monkey", "horse"),
+		expectedError: "types does not support hashcode yet: VARCHAR",
+	}}
+
+	for _, tc := range testCases {
+		t.Run(tc.testName+"-Execute", func(t *testing.T) {
+			distinct := &Distinct{Source: &fakePrimitive{results: []*sqltypes.Result{tc.inputs}}}
+
+			qr, err := distinct.Execute(&noopVCursor{ctx: context.Background()}, nil, true)
+			if tc.expectedError == "" {
+				require.NoError(t, err)
+				got := fmt.Sprintf("%v", qr.Rows)
+				expected := fmt.Sprintf("%v", tc.expectedResult.Rows)
+				utils.MustMatch(t, expected, got, "result not what correct")
+			} else {
+				require.EqualError(t, err, tc.expectedError)
+			}
+		})
+		t.Run(tc.testName+"-StreamExecute", func(t *testing.T) {
+			distinct := &Distinct{Source: &fakePrimitive{results: []*sqltypes.Result{tc.inputs}}}
+
+			result, err := wrapStreamExecute(distinct, &noopVCursor{ctx: context.Background()}, nil, true)
+
+			if tc.expectedError == "" {
+				require.NoError(t, err)
+				got := fmt.Sprintf("%v", result.Rows)
+				expected := fmt.Sprintf("%v", tc.expectedResult.Rows)
+				utils.MustMatch(t, expected, got, "result not what correct")
+			} else {
+				require.EqualError(t, err, tc.expectedError)
+			}
+		})
+	}
+}
diff --git a/go/vt/vtgate/executor_select_test.go b/go/vt/vtgate/executor_select_test.go
index c5b4c0c071..6bc0fd3531 100644
--- a/go/vt/vtgate/executor_select_test.go
+++ b/go/vt/vtgate/executor_select_test.go
@@ -457,7 +457,7 @@ func TestSelectLastInsertIdInUnion(t *testing.T) {
 	sql := "select last_insert_id() as id union select id from user"
 	_, err := executorExec(executor, sql, map[string]*querypb.BindVariable{})
 	require.Error(t, err)
-	assert.Contains(t, err.Error(), "unsupported: UNION cannot be executed as a single route")
+	assert.Contains(t, err.Error(), "types does not support hashcode yet: VARCHAR")
 }
 
 func TestSelectLastInsertIdInWhere(t *testing.T) {
diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go
index ab4084e02d..d128b3050b 100644
--- a/go/vt/vtgate/planbuilder/plan_test.go
+++ b/go/vt/vtgate/planbuilder/plan_test.go
@@ -153,7 +153,11 @@ func TestPlan(t *testing.T) {
 
 	testOutputTempDir, err := ioutil.TempDir("", "plan_test")
 	require.NoError(t, err)
-	defer os.RemoveAll(testOutputTempDir)
+	defer func() {
+		if !t.Failed() {
+			os.RemoveAll(testOutputTempDir)
+		}
+	}()
 	// You will notice that some tests expect user.Id instead of user.id.
 	// This is because we now pre-create vindex columns in the symbol
 	// table, which come from vschema. In the test vschema,
diff --git a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
index 1172ea4dcf..9543131bd5 100644
--- a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
@@ -114,6 +114,7 @@
           "Sharded": true
         },
         "FieldQuery": "select count(*), a, textcol1, b, weight_string(textcol1) from user where 1 != 1 group by a, textcol1, b",
+        "OrderBy": "1 ASC, 4 ASC, 3 ASC",
         "Query": "select count(*), a, textcol1, b, weight_string(textcol1) from user group by a, textcol1, b order by a asc, textcol1 asc, b asc",
         "Table": "user"
       }
@@ -146,6 +147,7 @@
               "Sharded": true
             },
             "FieldQuery": "select count(*) as k, a, textcol1, b, weight_string(textcol1) from user where 1 != 1 group by a, textcol1, b",
+            "OrderBy": "4 ASC, 1 ASC, 3 ASC",
             "Query": "select count(*) as k, a, textcol1, b, weight_string(textcol1) from user group by a, textcol1, b order by textcol1 asc, a asc, b asc",
             "Table": "user"
           }
@@ -278,6 +280,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col1, col2 from user where 1 != 1 group by col1",
+        "OrderBy": "0 ASC, 1 ASC, 0 ASC",
         "Query": "select distinct col1, col2 from user group by col1 order by col1 asc, col2 asc, col1 asc",
         "Table": "user"
       }
@@ -378,6 +381,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col, count(*) from user where 1 != 1 group by col",
+        "OrderBy": "0 ASC",
         "Query": "select col, count(*) from user group by col order by col asc",
         "Table": "user"
       }
@@ -405,6 +409,7 @@
           "Sharded": true
         },
         "FieldQuery": "select name, count(*) from user where 1 != 1 group by name",
+        "OrderBy": "0 ASC",
         "Query": "select name, count(*) from user group by name order by name asc",
         "Table": "user"
       }
@@ -585,6 +590,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col from user where 1 != 1",
+        "OrderBy": "0 ASC",
         "Query": "select distinct col from user order by col asc",
         "Table": "user"
       }
@@ -611,6 +617,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col from user where 1 != 1 group by col",
+        "OrderBy": "0 ASC",
         "Query": "select col from user group by col order by col asc",
         "Table": "user"
       }
@@ -656,6 +663,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col, count(distinct id) from user where 1 != 1 group by col",
+        "OrderBy": "0 ASC",
         "Query": "select col, count(distinct id) from user group by col order by col asc",
         "Table": "user"
       }
@@ -683,6 +691,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col1, col2 from user where 1 != 1 group by col1, col2",
+        "OrderBy": "0 ASC, 1 ASC",
         "Query": "select col1, col2 from user group by col1, col2 order by col1 asc, col2 asc",
         "Table": "user"
       }
@@ -709,6 +718,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col2 from user where 1 != 1 group by col2",
+        "OrderBy": "0 ASC",
         "Query": "select col2 from user group by col2 order by col2 asc",
         "Table": "user"
       }
@@ -736,6 +746,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col1, col2 from user where 1 != 1 group by col1, col2",
+        "OrderBy": "0 ASC, 1 ASC",
         "Query": "select col1, col2 from user group by col1, col2 order by col1 asc, col2 asc",
         "Table": "user"
       }
@@ -763,6 +774,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col1, col2 from user where 1 != 1 group by col1, col2",
+        "OrderBy": "0 ASC, 1 ASC",
         "Query": "select col1, col2 from user group by col1, col2 order by col1 asc, col2 asc",
         "Table": "user"
       }
@@ -790,6 +802,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col1, min(distinct col2) from user where 1 != 1 group by col1",
+        "OrderBy": "0 ASC",
         "Query": "select col1, min(distinct col2) from user group by col1 order by col1 asc",
         "Table": "user"
       }
@@ -822,6 +835,7 @@
               "Sharded": true
             },
             "FieldQuery": "select col1, col2 from user where 1 != 1 group by col1, col2",
+            "OrderBy": "0 ASC, 1 ASC",
             "Query": "select col1, col2 from user group by col1, col2 order by col1 asc, col2 asc",
             "Table": "user"
           }
@@ -855,6 +869,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, count(*) from user where 1 != 1 group by b, a",
+        "OrderBy": "1 ASC, 0 ASC",
         "Query": "select a, b, count(*) from user group by b, a order by b asc, a asc",
         "Table": "user"
       }
@@ -882,6 +897,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, count(*) from user where 1 != 1 group by 2, 1",
+        "OrderBy": "1 ASC, 0 ASC",
         "Query": "select a, b, count(*) from user group by 2, 1 order by b asc, a asc",
         "Table": "user"
       }
@@ -908,6 +924,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col from user where 1 != 1 group by 1",
+        "OrderBy": "0 ASC",
         "Query": "select col from user group by 1 order by col asc",
         "Table": "user"
       }
@@ -969,6 +986,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, c, d, count(*) from user where 1 != 1 group by 1, 2, 3",
+        "OrderBy": "0 ASC, 1 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*) from user group by 1, 2, 3 order by 1 asc, 2 asc, 3 asc",
         "Table": "user"
       }
@@ -996,6 +1014,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, c, d, count(*) from user where 1 != 1 group by 1, 2, 3",
+        "OrderBy": "0 ASC, 1 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*) from user group by 1, 2, 3 order by a asc, b asc, c asc",
         "Table": "user"
       }
@@ -1023,6 +1042,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, c, d, count(*) from user where 1 != 1 group by 1, 2, 3, 4",
+        "OrderBy": "3 ASC, 1 ASC, 0 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*) from user group by 1, 2, 3, 4 order by d asc, b asc, a asc, c asc",
         "Table": "user"
       }
@@ -1050,6 +1070,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, c, d, count(*) from user where 1 != 1 group by 3, 2, 1, 4",
+        "OrderBy": "3 ASC, 1 ASC, 0 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*) from user group by 3, 2, 1, 4 order by d asc, b asc, a asc, c asc",
         "Table": "user"
       }
@@ -1077,6 +1098,7 @@
           "Sharded": true
         },
         "FieldQuery": "select a, b, c, count(*) from user where 1 != 1 group by 3, 2, 1",
+        "OrderBy": "0 DESC, 2 DESC, 1 ASC",
         "Query": "select a, b, c, count(*) from user group by 3, 2, 1 order by 1 desc, 3 desc, b asc",
         "Table": "user"
       }
@@ -1112,6 +1134,7 @@
               "Sharded": true
             },
             "FieldQuery": "select col, count(*) from user where 1 != 1 group by col",
+            "OrderBy": "0 ASC",
             "Query": "select col, count(*) from user group by col order by col asc limit :__upper_limit",
             "Table": "user"
           }
diff --git a/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt b/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt
index 50c7e60b86..dbdb88abe4 100644
--- a/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt
@@ -24,6 +24,7 @@
               "Sharded": true
             },
             "FieldQuery": "select a, b, count(*) from user where 1 != 1 group by a",
+            "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) from user group by a order by a asc",
             "Table": "user"
           }
@@ -58,6 +59,7 @@
               "Sharded": true
             },
             "FieldQuery": "select a, b, count(*) as k from user where 1 != 1 group by a",
+            "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) as k from user group by a order by a asc",
             "Table": "user"
           }
@@ -92,6 +94,7 @@
               "Sharded": true
             },
             "FieldQuery": "select a, b, count(*) as k from user where 1 != 1 group by a",
+            "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) as k from user group by a order by a asc",
             "Table": "user"
           }
@@ -130,6 +133,7 @@
                   "Sharded": true
                 },
                 "FieldQuery": "select a, b, count(*) as k from user where 1 != 1 group by a",
+                "OrderBy": "0 ASC",
                 "Query": "select a, b, count(*) as k from user group by a order by a asc",
                 "Table": "user"
               }
@@ -166,6 +170,7 @@
               "Sharded": true
             },
             "FieldQuery": "select a, b, count(*) as k from user where 1 != 1 group by a",
+            "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) as k from user group by a order by 1 asc",
             "Table": "user"
           }
@@ -201,6 +206,7 @@
               "Sharded": true
             },
             "FieldQuery": "select textcol1, count(*) as k, weight_string(textcol1) from user where 1 != 1 group by textcol1",
+            "OrderBy": "2 ASC, 2 ASC",
             "Query": "select textcol1, count(*) as k, weight_string(textcol1) from user group by textcol1 order by textcol1 asc, textcol1 asc",
             "Table": "user"
           }
diff --git a/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt b/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
index 8d0be93157..ff2628fb9b 100644
--- a/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
@@ -190,6 +190,7 @@
       "Sharded": true
     },
     "FieldQuery": "select col from user where 1 != 1",
+    "OrderBy": "0 ASC",
     "Query": "select col from user order by col asc",
     "Table": "user"
   }
@@ -208,6 +209,7 @@
       "Sharded": true
     },
     "FieldQuery": "select user_id, col1, col2 from authoritative where 1 != 1",
+    "OrderBy": "0 ASC",
     "Query": "select user_id, col1, col2 from authoritative order by user_id asc",
     "Table": "authoritative"
   }
@@ -226,6 +228,7 @@
       "Sharded": true
     },
     "FieldQuery": "select user_id, col1, col2, weight_string(col1) from authoritative where 1 != 1",
+    "OrderBy": "3 ASC",
     "Query": "select user_id, col1, col2, weight_string(col1) from authoritative order by col1 asc",
     "Table": "authoritative"
   }
@@ -244,6 +247,7 @@
       "Sharded": true
     },
     "FieldQuery": "select a, textcol1, b, weight_string(textcol1) from user where 1 != 1",
+    "OrderBy": "0 ASC, 3 ASC, 2 ASC",
     "Query": "select a, textcol1, b, weight_string(textcol1) from user order by a asc, textcol1 asc, b asc",
     "Table": "user"
   }
@@ -262,6 +266,7 @@
       "Sharded": true
     },
     "FieldQuery": "select a, user.textcol1, b, weight_string(user.textcol1) from user where 1 != 1",
+    "OrderBy": "0 ASC, 3 ASC, 2 ASC",
     "Query": "select a, user.textcol1, b, weight_string(user.textcol1) from user order by a asc, textcol1 asc, b asc",
     "Table": "user"
   }
@@ -280,6 +285,7 @@
       "Sharded": true
     },
     "FieldQuery": "select a, textcol1, b, textcol2, weight_string(textcol1), weight_string(textcol2) from user where 1 != 1",
+    "OrderBy": "0 ASC, 4 ASC, 2 ASC, 5 ASC",
     "Query": "select a, textcol1, b, textcol2, weight_string(textcol1), weight_string(textcol2) from user order by a asc, textcol1 asc, b asc, textcol2 asc",
     "Table": "user"
   }
@@ -335,6 +341,7 @@
           "Sharded": true
         },
         "FieldQuery": "select col from user where 1 != 1",
+        "OrderBy": "0 ASC",
         "Query": "select col from user where :__sq_has_values1 = 1 and col in ::__sq1 order by col asc",
         "Table": "user"
       }
diff --git a/go/vt/vtgate/planbuilder/testdata/select_cases.txt b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
index ca6c851e09..d93be3a39e 100644
--- a/go/vt/vtgate/planbuilder/testdata/select_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
@@ -840,6 +840,7 @@
           "Sharded": true
         },
         "FieldQuery": "select user_id from music where 1 != 1",
+        "OrderBy": "0 ASC",
         "Query": "select user_id from music order by user_id asc limit :__upper_limit",
         "Table": "music"
       }
@@ -1520,6 +1521,7 @@
               "Sharded": true
             },
             "FieldQuery": "select user_id, count(id) from music where 1 != 1 group by user_id",
+            "OrderBy": "0 ASC",
             "Query": "select user_id, count(id) from music group by user_id having count(user_id) = 1 order by user_id asc limit :__upper_limit",
             "Table": "music"
           }
diff --git a/go/vt/vtgate/planbuilder/testdata/union_cases.txt b/go/vt/vtgate/planbuilder/testdata/union_cases.txt
index 125cba7e7b..493f68f1d0 100644
--- a/go/vt/vtgate/planbuilder/testdata/union_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/union_cases.txt
@@ -16,6 +16,45 @@
   }
 }
 
+# union distinct between two scatter selects
+"select id from user union select id from music"
+{
+  "QueryType": "SELECT",
+  "Original": "select id from user union select id from music",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select id from user where 1 != 1",
+            "Query": "select id from user",
+            "Table": "user"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select id from music where 1 != 1",
+            "Query": "select id from music",
+            "Table": "music"
+          }
+        ]
+      }
+    ]
+  }
+}
+
 # union all between two SelectEqualUnique
 "select id from user where id = 1 union all select id from user where id = 5"
 {
@@ -78,6 +117,7 @@
               "Sharded": true
             },
             "FieldQuery": "select id from user where 1 != 1",
+            "OrderBy": "0 DESC",
             "Query": "select id from user order by id desc limit :__upper_limit",
             "Table": "user"
           }
@@ -95,6 +135,7 @@
               "Sharded": true
             },
             "FieldQuery": "select id from music where 1 != 1",
+            "OrderBy": "0 DESC",
             "Query": "select id from music order by id desc limit :__upper_limit",
             "Table": "music"
           }
@@ -160,6 +201,7 @@
               "Sharded": true
             },
             "FieldQuery": "select id from user where 1 != 1",
+            "OrderBy": "0 ASC",
             "Query": "select id from user order by id asc limit :__upper_limit",
             "Table": "user"
           }
@@ -177,6 +219,7 @@
               "Sharded": true
             },
             "FieldQuery": "select id from music where 1 != 1",
+            "OrderBy": "0 DESC",
             "Query": "select id from music order by id desc limit :__upper_limit",
             "Table": "music"
           }
@@ -223,3 +266,544 @@
     ]
   }
 }
+
+# union of information_schema with normal table
+"select * from information_schema.a union select * from unsharded"
+{
+  "QueryType": "SELECT",
+  "Original": "select * from information_schema.a union select * from unsharded",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectDBA",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select * from information_schema.a where 1 != 1",
+            "Query": "select * from information_schema.a"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectUnsharded",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select * from unsharded where 1 != 1",
+            "Query": "select * from unsharded",
+            "Table": "unsharded"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# union of information_schema with normal table
+"select * from unsharded union select * from information_schema.a"
+{
+  "QueryType": "SELECT",
+  "Original": "select * from unsharded union select * from information_schema.a",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectUnsharded",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select * from unsharded where 1 != 1",
+            "Query": "select * from unsharded",
+            "Table": "unsharded"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectDBA",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select * from information_schema.a where 1 != 1",
+            "Query": "select * from information_schema.a"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# multi-shard union
+"(select id from user union select id from music) union select 1 from dual"
+{
+  "QueryType": "SELECT",
+  "Original": "(select id from user union select id from music) union select 1 from dual",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Distinct",
+            "Inputs": [
+              {
+                "OperatorType": "Concatenate",
+                "Inputs": [
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "SelectScatter",
+                    "Keyspace": {
+                      "Name": "user",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select id from user where 1 != 1",
+                    "Query": "select id from user",
+                    "Table": "user"
+                  },
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "SelectScatter",
+                    "Keyspace": {
+                      "Name": "user",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select id from music where 1 != 1",
+                    "Query": "select id from music",
+                    "Table": "music"
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectReference",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select 1 from dual where 1 != 1",
+            "Query": "select 1 from dual",
+            "Table": "dual"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# multi-shard union
+"select 1 from music union (select id from user union all select name from unsharded)"
+{
+  "QueryType": "SELECT",
+  "Original": "select 1 from music union (select id from user union all select name from unsharded)",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 1 from music where 1 != 1",
+            "Query": "select 1 from music",
+            "Table": "music"
+          },
+          {
+            "OperatorType": "Concatenate",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "SelectScatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select id from user where 1 != 1",
+                "Query": "select id from user",
+                "Table": "user"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "SelectUnsharded",
+                "Keyspace": {
+                  "Name": "main",
+                  "Sharded": false
+                },
+                "FieldQuery": "select name from unsharded where 1 != 1",
+                "Query": "select name from unsharded",
+                "Table": "unsharded"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# multi-shard union
+"select 1 from music union (select id from user union select name from unsharded)"
+{
+  "QueryType": "SELECT",
+  "Original": "select 1 from music union (select id from user union select name from unsharded)",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 1 from music where 1 != 1",
+            "Query": "select 1 from music",
+            "Table": "music"
+          },
+          {
+            "OperatorType": "Distinct",
+            "Inputs": [
+              {
+                "OperatorType": "Concatenate",
+                "Inputs": [
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "SelectScatter",
+                    "Keyspace": {
+                      "Name": "user",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select id from user where 1 != 1",
+                    "Query": "select id from user",
+                    "Table": "user"
+                  },
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "SelectUnsharded",
+                    "Keyspace": {
+                      "Name": "main",
+                      "Sharded": false
+                    },
+                    "FieldQuery": "select name from unsharded where 1 != 1",
+                    "Query": "select name from unsharded",
+                    "Table": "unsharded"
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# union with the same target shard because of vindex
+"select * from music where id = 1 union select * from user where id = 1"
+{
+  "QueryType": "SELECT",
+  "Original": "select * from music where id = 1 union select * from user where id = 1",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectEqualUnique",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select * from music where 1 != 1",
+            "Query": "select * from music where id = 1",
+            "Table": "music",
+            "Values": [
+              1
+            ],
+            "Vindex": "music_user_map"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectEqualUnique",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select * from user where 1 != 1",
+            "Query": "select * from user where id = 1",
+            "Table": "user",
+            "Values": [
+              1
+            ],
+            "Vindex": "user_index"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# union with different target shards
+"select 1 from music where id = 1 union select 1 from music where id = 2"
+{
+  "QueryType": "SELECT",
+  "Original": "select 1 from music where id = 1 union select 1 from music where id = 2",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectEqualUnique",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 1 from music where 1 != 1",
+            "Query": "select 1 from music where id = 1",
+            "Table": "music",
+            "Values": [
+              1
+            ],
+            "Vindex": "music_user_map"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectEqualUnique",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 1 from music where 1 != 1",
+            "Query": "select 1 from music where id = 2",
+            "Table": "music",
+            "Values": [
+              2
+            ],
+            "Vindex": "music_user_map"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# multiple select statement have inner order by with union - TODO (systay) no need to send down ORDER BY if we are going to loose it with UNION DISTINCT
+"(select 1 from user order by 1 desc) union (select 1 from user order by 1 asc)"
+{
+  "QueryType": "SELECT",
+  "Original": "(select 1 from user order by 1 desc) union (select 1 from user order by 1 asc)",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 1 from user where 1 != 1",
+            "OrderBy": "0 DESC",
+            "Query": "select 1 from user order by 1 desc",
+            "Table": "user"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 1 from user where 1 != 1",
+            "OrderBy": "0 ASC",
+            "Query": "select 1 from user order by 1 asc",
+            "Table": "user"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# multiple unions
+"select 1 union select null union select 1.0 union select '1' union select 2 union select 2.0 from user"
+{
+  "QueryType": "SELECT",
+  "Original": "select 1 union select null union select 1.0 union select '1' union select 2 union select 2.0 from user",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectReference",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select 1 from dual where 1 != 1 union select null from dual where 1 != 1 union select 1.0 from dual where 1 != 1 union select '1' from dual where 1 != 1 union select 2 from dual where 1 != 1",
+            "Query": "select 1 from dual union select null from dual union select 1.0 from dual union select '1' from dual union select 2 from dual",
+            "Table": "dual"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 2.0 from user where 1 != 1",
+            "Query": "select 2.0 from user",
+            "Table": "user"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# union distinct between a scatter query and a join (other side)
+"(select user.id, user.name from user join user_extra where user_extra.extra = 'asdf') union select 'b','c' from user"
+{
+  "QueryType": "SELECT",
+  "Original": "(select user.id, user.name from user join user_extra where user_extra.extra = 'asdf') union select 'b','c' from user",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Join",
+            "Variant": "Join",
+            "JoinColumnIndexes": "-1,-2",
+            "TableName": "user_user_extra",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "SelectScatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select user.id, user.name from user where 1 != 1",
+                "Query": "select user.id, user.name from user",
+                "Table": "user"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "SelectScatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select 1 from user_extra where 1 != 1",
+                "Query": "select 1 from user_extra where user_extra.extra = 'asdf'",
+                "Table": "user_extra"
+              }
+            ]
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 'b', 'c' from user where 1 != 1",
+            "Query": "select 'b', 'c' from user",
+            "Table": "user"
+          }
+        ]
+      }
+    ]
+  }
+}
+
+# union distinct between a scatter query and a join (other side)
+"select 'b','c' from user union (select user.id, user.name from user join user_extra where user_extra.extra = 'asdf')"
+{
+  "QueryType": "SELECT",
+  "Original": "select 'b','c' from user union (select user.id, user.name from user join user_extra where user_extra.extra = 'asdf')",
+  "Instructions": {
+    "OperatorType": "Distinct",
+    "Inputs": [
+      {
+        "OperatorType": "Concatenate",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "SelectScatter",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select 'b', 'c' from user where 1 != 1",
+            "Query": "select 'b', 'c' from user",
+            "Table": "user"
+          },
+          {
+            "OperatorType": "Join",
+            "Variant": "Join",
+            "JoinColumnIndexes": "-1,-2",
+            "TableName": "user_user_extra",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "SelectScatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select user.id, user.name from user where 1 != 1",
+                "Query": "select user.id, user.name from user",
+                "Table": "user"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "SelectScatter",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select 1 from user_extra where 1 != 1",
+                "Query": "select 1 from user_extra where user_extra.extra = 'asdf'",
+                "Table": "user_extra"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
index 320a834fc6..a52531c885 100644
--- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
@@ -1,14 +1,10 @@
-# Unions
-"select * from user union select * from user_extra"
-"unsupported: UNION cannot be executed as a single route"
-
 # SHOW
 "show create database"
 "plan building not supported"
 
 # union operations in subqueries (expressions)
 "select * from user where id in (select * from user union select * from user_extra)"
-"unsupported: UNION cannot be executed as a single route"
+"unsupported: '*' expression in cross-shard query"
 
 # TODO: Implement support for select with a target destination
 "select * from `user[-]`.user_metadata"
@@ -318,40 +314,6 @@
 "replace into user(id) values (1), (2)"
 "unsupported: REPLACE INTO with sharded schema"
 
-# union of information_schema with normal table
-"select * from information_schema.a union select * from unsharded"
-"unsupported: UNION cannot be executed as a single route"
-
-# union of information_schema with normal table
-"select * from unsharded union select * from information_schema.a"
-"unsupported: UNION cannot be executed as a single route"
-
-# multi-shard union
-"(select id from user union select id from music) union select 1 from dual"
-"unsupported: UNION cannot be executed as a single route"
-
-# multi-shard union
-"select 1 from music union (select id from user union all select name from unsharded)"
-"unsupported: SELECT of UNION is non-trivial"
-
-# multi-shard union
-"select 1 from music union (select id from user union select name from unsharded)"
-"unsupported: UNION cannot be executed as a single route"
-
-# union with the same target shard because of vindex
-"select * from music where id = 1 union select * from user where id = 1"
-"unsupported: UNION cannot be executed as a single route"
-
-# union with different target shards
-"select 1 from music where id = 1 union select 1 from music where id = 2"
-"unsupported: UNION cannot be executed as a single route"
-
-"(select user.id, user.name from user join user_extra where user_extra.extra = 'asdf') union select 'b','c' from user"
-"unsupported: SELECT of UNION is non-trivial"
-
-"select 'b','c' from user union (select user.id, user.name from user join user_extra where user_extra.extra = 'asdf')"
-"unsupported: SELECT of UNION is non-trivial"
-
 "select keyspace_id from user_index where id = 1 and id = 2"
 "unsupported: where clause for vindex function must be of the form id = <val> (multiple filters)"
 
@@ -392,10 +354,6 @@
 "(select 1 from user order by 1 desc) order by 1 asc limit 2"
 "can't do ORDER BY on top of ORDER BY"
 
-# multiple select statement have inner order by with union
-"(select 1 from user order by 1 desc) union (select 1 from user order by 1 asc)"
-"unsupported: SELECT of UNION is non-trivial"
-
 # different number of columns
 "select id, 42 from user where id = 1 union all select id from user where id = 5"
 "The used SELECT statements have a different number of columns (errno 1222) (sqlstate 21000) during query: select id, 42 from user where id = 1 union all select id from user where id = 5"
@@ -435,4 +393,3 @@
 # Multi shard query using into outfile s3
 "select * from user into outfile s3 'out_file_name'"
 "unsupported: this construct is not supported on sharded keyspace"
-