Repository: vitessio/vitess
Pull request #5575 (2019-12-15 05:36:12)

vrepl: choose best vindex instead of primary
The primary vindex is not always the best vindex for vreplication. This change makes sure we choose the best possible vindex.

As part of this change, the vindex API requires you to publish whether it makes use of VCursor via `NeedsVCursor`. This information is used by vreplication to eliminate the lookup vindexes from the list of eligible vindexes.

In the future, there can be lookup vindexes that don't use VCursor. Such vindexes can still be used by vreplication. There will be a performance impact, but it will at least be possible,


Patch:
diff --git a/go.mod b/go.mod
index af52af7313..9b281e72f5 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,6 @@ 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
@@ -50,8 +49,6 @@ require (
 	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/vtgate/vindexes/binary.go b/go/vt/vtgate/vindexes/binary.go
index 50d0500a9b..2affb723c0 100644
--- a/go/vt/vtgate/vindexes/binary.go
+++ b/go/vt/vtgate/vindexes/binary.go
@@ -54,6 +54,11 @@ func (vind *Binary) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *Binary) NeedsVCursor() bool {
+	return false
+}
+
 // Verify returns true if ids maps to ksids.
 func (vind *Binary) Verify(_ VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	out := make([]bool, len(ids))
diff --git a/go/vt/vtgate/vindexes/binarymd5.go b/go/vt/vtgate/vindexes/binarymd5.go
index be6cea7931..c2327b9ae2 100644
--- a/go/vt/vtgate/vindexes/binarymd5.go
+++ b/go/vt/vtgate/vindexes/binarymd5.go
@@ -53,6 +53,11 @@ func (vind *BinaryMD5) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *BinaryMD5) NeedsVCursor() bool {
+	return false
+}
+
 // Verify returns true if ids maps to ksids.
 func (vind *BinaryMD5) Verify(_ VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	out := make([]bool, len(ids))
diff --git a/go/vt/vtgate/vindexes/consistent_lookup.go b/go/vt/vtgate/vindexes/consistent_lookup.go
index 6d862e7c8c..a2bc94915e 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup.go
@@ -75,6 +75,11 @@ func (lu *ConsistentLookup) IsUnique() bool {
 	return false
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lu *ConsistentLookup) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lu *ConsistentLookup) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
@@ -129,6 +134,11 @@ func (lu *ConsistentLookupUnique) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lu *ConsistentLookupUnique) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lu *ConsistentLookupUnique) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
diff --git a/go/vt/vtgate/vindexes/hash.go b/go/vt/vtgate/vindexes/hash.go
index 1811750402..21b93dfe88 100644
--- a/go/vt/vtgate/vindexes/hash.go
+++ b/go/vt/vtgate/vindexes/hash.go
@@ -64,6 +64,11 @@ func (vind *Hash) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *Hash) NeedsVCursor() bool {
+	return false
+}
+
 // Map can map ids to key.Destination objects.
 func (vind *Hash) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, len(ids))
diff --git a/go/vt/vtgate/vindexes/lookup.go b/go/vt/vtgate/vindexes/lookup.go
index 6f13abb0e5..5474cc1788 100644
--- a/go/vt/vtgate/vindexes/lookup.go
+++ b/go/vt/vtgate/vindexes/lookup.go
@@ -61,6 +61,11 @@ func (ln *LookupNonUnique) IsUnique() bool {
 	return false
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (ln *LookupNonUnique) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (ln *LookupNonUnique) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
@@ -211,6 +216,11 @@ func (lu *LookupUnique) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lu *LookupUnique) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lu *LookupUnique) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
diff --git a/go/vt/vtgate/vindexes/lookup_hash.go b/go/vt/vtgate/vindexes/lookup_hash.go
index 46809ad4b7..a9d2ec0102 100644
--- a/go/vt/vtgate/vindexes/lookup_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_hash.go
@@ -93,6 +93,11 @@ func (lh *LookupHash) IsUnique() bool {
 	return false
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lh *LookupHash) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lh *LookupHash) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
@@ -244,6 +249,11 @@ func (lhu *LookupHashUnique) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lhu *LookupHashUnique) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lhu *LookupHashUnique) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
diff --git a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
index f56d302c81..fccec4ec11 100644
--- a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
@@ -94,6 +94,11 @@ func (lh *LookupUnicodeLooseMD5Hash) IsUnique() bool {
 	return false
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lh *LookupUnicodeLooseMD5Hash) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lh *LookupUnicodeLooseMD5Hash) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
@@ -256,6 +261,11 @@ func (lhu *LookupUnicodeLooseMD5HashUnique) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (lhu *LookupUnicodeLooseMD5HashUnique) NeedsVCursor() bool {
+	return true
+}
+
 // Map can map ids to key.Destination objects.
 func (lhu *LookupUnicodeLooseMD5HashUnique) Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
diff --git a/go/vt/vtgate/vindexes/null.go b/go/vt/vtgate/vindexes/null.go
index d3fc3fc56b..5f8b9b7f1a 100644
--- a/go/vt/vtgate/vindexes/null.go
+++ b/go/vt/vtgate/vindexes/null.go
@@ -48,9 +48,9 @@ func (vind *Null) String() string {
 	return vind.name
 }
 
-// Cost returns the cost of this index as 0.
+// Cost returns the cost of this index as 100.
 func (vind *Null) Cost() int {
-	return 0
+	return 100
 }
 
 // IsUnique returns true since the Vindex is unique.
@@ -58,6 +58,11 @@ func (vind *Null) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *Null) NeedsVCursor() bool {
+	return false
+}
+
 // Map can map ids to key.Destination objects.
 func (vind *Null) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, 0, len(ids))
diff --git a/go/vt/vtgate/vindexes/numeric.go b/go/vt/vtgate/vindexes/numeric.go
index 8ebdbe5c2d..3b34b8a229 100644
--- a/go/vt/vtgate/vindexes/numeric.go
+++ b/go/vt/vtgate/vindexes/numeric.go
@@ -57,6 +57,11 @@ func (*Numeric) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (*Numeric) NeedsVCursor() bool {
+	return false
+}
+
 // Verify returns true if ids and ksids match.
 func (*Numeric) Verify(_ VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	out := make([]bool, len(ids))
diff --git a/go/vt/vtgate/vindexes/numeric_static_map.go b/go/vt/vtgate/vindexes/numeric_static_map.go
index 39832aa377..6a1ac7ecbc 100644
--- a/go/vt/vtgate/vindexes/numeric_static_map.go
+++ b/go/vt/vtgate/vindexes/numeric_static_map.go
@@ -80,6 +80,11 @@ func (vind *NumericStaticMap) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *NumericStaticMap) NeedsVCursor() bool {
+	return false
+}
+
 // Verify returns true if ids and ksids match.
 func (vind *NumericStaticMap) Verify(_ VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	out := make([]bool, len(ids))
diff --git a/go/vt/vtgate/vindexes/region_experimental.go b/go/vt/vtgate/vindexes/region_experimental.go
index ba10a10f7d..340e880e24 100644
--- a/go/vt/vtgate/vindexes/region_experimental.go
+++ b/go/vt/vtgate/vindexes/region_experimental.go
@@ -79,6 +79,11 @@ func (ge *RegionExperimental) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (ge *RegionExperimental) NeedsVCursor() bool {
+	return false
+}
+
 // Map satisfies MultiColumn.
 func (ge *RegionExperimental) Map(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
 	destinations := make([]key.Destination, 0, len(rowsColValues))
diff --git a/go/vt/vtgate/vindexes/reverse_bits.go b/go/vt/vtgate/vindexes/reverse_bits.go
index f199eaaea3..d79d1f8a23 100644
--- a/go/vt/vtgate/vindexes/reverse_bits.go
+++ b/go/vt/vtgate/vindexes/reverse_bits.go
@@ -59,6 +59,11 @@ func (vind *ReverseBits) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *ReverseBits) NeedsVCursor() bool {
+	return false
+}
+
 // Map returns the corresponding KeyspaceId values for the given ids.
 func (vind *ReverseBits) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, len(ids))
diff --git a/go/vt/vtgate/vindexes/unicodeloosemd5.go b/go/vt/vtgate/vindexes/unicodeloosemd5.go
index ef3a70c9f1..c62cf0ff80 100644
--- a/go/vt/vtgate/vindexes/unicodeloosemd5.go
+++ b/go/vt/vtgate/vindexes/unicodeloosemd5.go
@@ -62,6 +62,11 @@ func (vind *UnicodeLooseMD5) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *UnicodeLooseMD5) NeedsVCursor() bool {
+	return false
+}
+
 // Verify returns true if ids maps to ksids.
 func (vind *UnicodeLooseMD5) Verify(_ VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	out := make([]bool, len(ids))
diff --git a/go/vt/vtgate/vindexes/vindex.go b/go/vt/vtgate/vindexes/vindex.go
index 3d06668def..ce7951aec6 100644
--- a/go/vt/vtgate/vindexes/vindex.go
+++ b/go/vt/vtgate/vindexes/vindex.go
@@ -56,8 +56,15 @@ type Vindex interface {
 	Cost() int
 
 	// IsUnique returns true if the Vindex is unique.
-	// Which means Map() maps to either a KeyRange or a single KeyspaceID.
+	// A Unique Vindex is allowed to return non-unique values like
+	// a keyrange. This is in situations where the vindex does not
+	// have enough information to map to a keyspace id. If so, such
+	// a vindex cannot be primary.
 	IsUnique() bool
+
+	// NeedsVCursor returns true if the Vindex makes calls into the
+	// VCursor. Such vindexes cannot be used by vreplication.
+	NeedsVCursor() bool
 }
 
 // SingleColumn defines the interface for a single column vindex.
diff --git a/go/vt/vtgate/vindexes/xxhash.go b/go/vt/vtgate/vindexes/xxhash.go
index fd648e3117..8f17730116 100644
--- a/go/vt/vtgate/vindexes/xxhash.go
+++ b/go/vt/vtgate/vindexes/xxhash.go
@@ -56,6 +56,11 @@ func (vind *XXHash) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (vind *XXHash) NeedsVCursor() bool {
+	return false
+}
+
 // Map can map ids to key.Destination objects.
 func (vind *XXHash) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
 	out := make([]key.Destination, len(ids))
diff --git a/go/vt/vttablet/tabletserver/vstreamer/local_vschema.go b/go/vt/vttablet/tabletserver/vstreamer/local_vschema.go
index e6e8c61eaa..57343bd654 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/local_vschema.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/local_vschema.go
@@ -31,16 +31,12 @@ type localVSchema struct {
 	vschema  *vindexes.VSchema
 }
 
-func (lvs *localVSchema) FindTable(tablename string) (*vindexes.Table, error) {
-	ks, ok := lvs.vschema.Keyspaces[lvs.keyspace]
-	if !ok {
-		return nil, fmt.Errorf("keyspace %s not found in vschema", lvs.keyspace)
-	}
-	table := ks.Tables[tablename]
-	if table == nil {
-		return nil, fmt.Errorf("table %s not found", tablename)
+func (lvs *localVSchema) FindColVindex(tablename string) (*vindexes.ColumnVindex, error) {
+	table, err := lvs.findTable(tablename)
+	if err != nil {
+		return nil, err
 	}
-	return table, nil
+	return identifyColVindex(table)
 }
 
 func (lvs *localVSchema) FindOrCreateVindex(qualifiedName string) (vindexes.Vindex, error) {
@@ -66,3 +62,37 @@ func (lvs *localVSchema) FindOrCreateVindex(qualifiedName string) (vindexes.Vind
 	}
 	return vindexes.CreateVindex(name, name, map[string]string{})
 }
+
+func (lvs *localVSchema) findTable(tablename string) (*vindexes.Table, error) {
+	ks, ok := lvs.vschema.Keyspaces[lvs.keyspace]
+	if !ok {
+		return nil, fmt.Errorf("keyspace %s not found in vschema", lvs.keyspace)
+	}
+	table := ks.Tables[tablename]
+	if table == nil {
+		return nil, fmt.Errorf("table %s not found", tablename)
+	}
+	return table, nil
+}
+
+func identifyColVindex(table *vindexes.Table) (*vindexes.ColumnVindex, error) {
+	if len(table.ColumnVindexes) == 0 {
+		return nil, fmt.Errorf("table %s has no vindex", table.Name.String())
+	}
+	var result *vindexes.ColumnVindex
+	for _, cv := range table.ColumnVindexes {
+		if cv.Vindex.NeedsVCursor() {
+			continue
+		}
+		if !cv.Vindex.IsUnique() {
+			continue
+		}
+		if result == nil || result.Vindex.Cost() > cv.Vindex.Cost() {
+			result = cv
+		}
+	}
+	if result == nil {
+		return nil, fmt.Errorf("could not find a vindex to compute keyspace id for table %v", table.Name.String())
+	}
+	return result, nil
+}
diff --git a/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go b/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
index dd5a58be79..3422463add 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
@@ -230,16 +230,12 @@ func buildREPlan(ti *Table, vschema *localVSchema, filter string) (*Plan, error)
 
 	// We need to additionally set VindexColumn, Vindex and KeyRange
 	// based on the Primary Vindex of the table.
-	table, err := vschema.FindTable(ti.Name)
+	cv, err := vschema.FindColVindex(ti.Name)
 	if err != nil {
 		return nil, err
 	}
-	// Get Primary Vindex.
-	if len(table.ColumnVindexes) == 0 {
-		return nil, fmt.Errorf("table %s has no primary vindex", ti.Name)
-	}
-	plan.Vindex = table.ColumnVindexes[0].Vindex
-	plan.VindexColumns, err = buildVindexColumns(plan.Table, table.ColumnVindexes[0].Columns)
+	plan.Vindex = cv.Vindex
+	plan.VindexColumns, err = buildVindexColumns(plan.Table, cv.Columns)
 	if err != nil {
 		return nil, err
 	}
@@ -365,20 +361,16 @@ func (plan *Plan) analyzeExpr(vschema *localVSchema, selExpr sqlparser.SelectExp
 		if len(inner.Exprs) != 0 {
 			return ColExpr{}, fmt.Errorf("unexpected: %v", sqlparser.String(inner))
 		}
-		table, err := vschema.FindTable(plan.Table.Name)
+		cv, err := vschema.FindColVindex(plan.Table.Name)
 		if err != nil {
 			return ColExpr{}, err
 		}
-		// Get Primary Vindex.
-		if len(table.ColumnVindexes) == 0 {
-			return ColExpr{}, fmt.Errorf("table %s has no primary vindex", plan.Table.Name)
-		}
-		vindexColumns, err := buildVindexColumns(plan.Table, table.ColumnVindexes[0].Columns)
+		vindexColumns, err := buildVindexColumns(plan.Table, cv.Columns)
 		if err != nil {
 			return ColExpr{}, err
 		}
 		return ColExpr{
-			Vindex:        table.ColumnVindexes[0].Vindex,
+			Vindex:        cv.Vindex,
 			VindexColumns: vindexColumns,
 			Alias:         sqlparser.NewColIdent("keyspace_id"),
 			Type:          sqltypes.VarBinary,
@@ -393,16 +385,12 @@ func (plan *Plan) analyzeInKeyRange(vschema *localVSchema, exprs sqlparser.Selec
 	var krExpr sqlparser.SelectExpr
 	switch {
 	case len(exprs) == 1:
-		table, err := vschema.FindTable(plan.Table.Name)
+		cv, err := vschema.FindColVindex(plan.Table.Name)
 		if err != nil {
 			return err
 		}
-		// Get Primary Vindex.
-		if len(table.ColumnVindexes) == 0 {
-			return fmt.Errorf("table %s has no primary vindex", plan.Table.Name)
-		}
-		colnames = table.ColumnVindexes[0].Columns
-		plan.Vindex = table.ColumnVindexes[0].Vindex
+		colnames = cv.Columns
+		plan.Vindex = cv.Vindex
 		krExpr = exprs[0]
 	case len(exprs) >= 3:
 		for _, expr := range exprs[:len(exprs)-2] {


Test patch:
diff --git a/go/vt/vtgate/engine/vindex_func_test.go b/go/vt/vtgate/engine/vindex_func_test.go
index 338ca9f7dd..ff22fe4e60 100644
--- a/go/vt/vtgate/engine/vindex_func_test.go
+++ b/go/vt/vtgate/engine/vindex_func_test.go
@@ -30,9 +30,10 @@ import (
 // uvindex is Unique.
 type uvindex struct{ matchid, matchkr bool }
 
-func (*uvindex) String() string { return "uvindex" }
-func (*uvindex) Cost() int      { return 1 }
-func (*uvindex) IsUnique() bool { return true }
+func (*uvindex) String() string     { return "uvindex" }
+func (*uvindex) Cost() int          { return 1 }
+func (*uvindex) IsUnique() bool     { return true }
+func (*uvindex) NeedsVCursor() bool { return false }
 func (*uvindex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	panic("unimplemented")
 }
@@ -59,9 +60,10 @@ func (v *uvindex) Map(cursor vindexes.VCursor, ids []sqltypes.Value) ([]key.Dest
 // nvindex is NonUnique.
 type nvindex struct{ matchid, matchkr bool }
 
-func (*nvindex) String() string { return "nvindex" }
-func (*nvindex) Cost() int      { return 1 }
-func (*nvindex) IsUnique() bool { return false }
+func (*nvindex) String() string     { return "nvindex" }
+func (*nvindex) Cost() int          { return 1 }
+func (*nvindex) IsUnique() bool     { return false }
+func (*nvindex) NeedsVCursor() bool { return false }
 func (*nvindex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	panic("unimplemented")
 }
diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go
index 1478144e5b..632de9c0bc 100644
--- a/go/vt/vtgate/executor_framework_test.go
+++ b/go/vt/vtgate/executor_framework_test.go
@@ -282,9 +282,10 @@ func (dp DestinationAnyShardPickerFirstShard) PickShard(shardCount int) int {
 type keyRangeLookuper struct {
 }
 
-func (v *keyRangeLookuper) String() string { return "keyrange_lookuper" }
-func (*keyRangeLookuper) Cost() int        { return 0 }
-func (*keyRangeLookuper) IsUnique() bool   { return false }
+func (v *keyRangeLookuper) String() string   { return "keyrange_lookuper" }
+func (*keyRangeLookuper) Cost() int          { return 0 }
+func (*keyRangeLookuper) IsUnique() bool     { return false }
+func (*keyRangeLookuper) NeedsVCursor() bool { return false }
 func (*keyRangeLookuper) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -306,9 +307,10 @@ func newKeyRangeLookuper(name string, params map[string]string) (vindexes.Vindex
 type keyRangeLookuperUnique struct {
 }
 
-func (v *keyRangeLookuperUnique) String() string { return "keyrange_lookuper" }
-func (*keyRangeLookuperUnique) Cost() int        { return 0 }
-func (*keyRangeLookuperUnique) IsUnique() bool   { return true }
+func (v *keyRangeLookuperUnique) String() string   { return "keyrange_lookuper" }
+func (*keyRangeLookuperUnique) Cost() int          { return 0 }
+func (*keyRangeLookuperUnique) IsUnique() bool     { return true }
+func (*keyRangeLookuperUnique) NeedsVCursor() bool { return false }
 func (*keyRangeLookuperUnique) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go
index 9e0e72ed30..885499d87a 100644
--- a/go/vt/vtgate/planbuilder/plan_test.go
+++ b/go/vt/vtgate/planbuilder/plan_test.go
@@ -39,9 +39,10 @@ import (
 // hashIndex is a functional, unique Vindex.
 type hashIndex struct{ name string }
 
-func (v *hashIndex) String() string { return v.name }
-func (*hashIndex) Cost() int        { return 1 }
-func (*hashIndex) IsUnique() bool   { return true }
+func (v *hashIndex) String() string   { return v.name }
+func (*hashIndex) Cost() int          { return 1 }
+func (*hashIndex) IsUnique() bool     { return true }
+func (*hashIndex) NeedsVCursor() bool { return false }
 func (*hashIndex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -56,9 +57,10 @@ func newHashIndex(name string, _ map[string]string) (vindexes.Vindex, error) {
 // lookupIndex is a unique Vindex, and satisfies Lookup.
 type lookupIndex struct{ name string }
 
-func (v *lookupIndex) String() string { return v.name }
-func (*lookupIndex) Cost() int        { return 2 }
-func (*lookupIndex) IsUnique() bool   { return true }
+func (v *lookupIndex) String() string   { return v.name }
+func (*lookupIndex) Cost() int          { return 2 }
+func (*lookupIndex) IsUnique() bool     { return true }
+func (*lookupIndex) NeedsVCursor() bool { return false }
 func (*lookupIndex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -80,9 +82,10 @@ var _ vindexes.Lookup = (*lookupIndex)(nil)
 // multiIndex satisfies Lookup, NonUnique.
 type multiIndex struct{ name string }
 
-func (v *multiIndex) String() string { return v.name }
-func (*multiIndex) Cost() int        { return 3 }
-func (*multiIndex) IsUnique() bool   { return false }
+func (v *multiIndex) String() string   { return v.name }
+func (*multiIndex) Cost() int          { return 3 }
+func (*multiIndex) IsUnique() bool     { return false }
+func (*multiIndex) NeedsVCursor() bool { return false }
 func (*multiIndex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -105,9 +108,10 @@ var _ vindexes.Lookup = (*multiIndex)(nil)
 // costlyIndex satisfies Lookup, NonUnique.
 type costlyIndex struct{ name string }
 
-func (v *costlyIndex) String() string { return v.name }
-func (*costlyIndex) Cost() int        { return 10 }
-func (*costlyIndex) IsUnique() bool   { return false }
+func (v *costlyIndex) String() string   { return v.name }
+func (*costlyIndex) Cost() int          { return 10 }
+func (*costlyIndex) IsUnique() bool     { return false }
+func (*costlyIndex) NeedsVCursor() bool { return false }
 func (*costlyIndex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
diff --git a/go/vt/vtgate/vindexes/binary_test.go b/go/vt/vtgate/vindexes/binary_test.go
index ac9d566609..a9c4554e12 100644
--- a/go/vt/vtgate/vindexes/binary_test.go
+++ b/go/vt/vtgate/vindexes/binary_test.go
@@ -19,9 +19,9 @@ package vindexes
 import (
 	"bytes"
 	"reflect"
-	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -33,16 +33,11 @@ func init() {
 	binOnlyVindex = vindex.(SingleColumn)
 }
 
-func TestBinaryCost(t *testing.T) {
-	if binOnlyVindex.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", binOnlyVindex.Cost())
-	}
-}
-
-func TestBinaryString(t *testing.T) {
-	if strings.Compare("binary_varchar", binOnlyVindex.String()) != 0 {
-		t.Errorf("String(): %s, want binary_varchar", binOnlyVindex.String())
-	}
+func TestBinaryInfo(t *testing.T) {
+	assert.Equal(t, 1, binOnlyVindex.Cost())
+	assert.Equal(t, "binary_varchar", binOnlyVindex.String())
+	assert.True(t, binOnlyVindex.IsUnique())
+	assert.False(t, binOnlyVindex.NeedsVCursor())
 }
 
 func TestBinaryMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/binarymd5_test.go b/go/vt/vtgate/vindexes/binarymd5_test.go
index f959fb4cd1..51d557003a 100644
--- a/go/vt/vtgate/vindexes/binarymd5_test.go
+++ b/go/vt/vtgate/vindexes/binarymd5_test.go
@@ -20,8 +20,7 @@ import (
 	"reflect"
 	"testing"
 
-	"strings"
-
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -33,16 +32,11 @@ func init() {
 	binVindex = vindex.(SingleColumn)
 }
 
-func TestBinaryMD5Cost(t *testing.T) {
-	if binVindex.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", binVindex.Cost())
-	}
-}
-
-func TestBinaryMD5String(t *testing.T) {
-	if strings.Compare("binary_md5_varchar", binVindex.String()) != 0 {
-		t.Errorf("String(): %s, want binary_md5_varchar", binVindex.String())
-	}
+func TestBinaryMD5Info(t *testing.T) {
+	assert.Equal(t, 1, binVindex.Cost())
+	assert.Equal(t, "binary_md5_varchar", binVindex.String())
+	assert.True(t, binVindex.IsUnique())
+	assert.False(t, binVindex.NeedsVCursor())
 }
 
 func TestBinaryMD5Map(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/consistent_lookup_test.go b/go/vt/vtgate/vindexes/consistent_lookup_test.go
index bb25c20427..b9cd9b4004 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup_test.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup_test.go
@@ -25,6 +25,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 	querypb "vitess.io/vitess/go/vt/proto/query"
@@ -46,28 +47,18 @@ func TestConsistentLookupInit(t *testing.T) {
 
 func TestConsistentLookupInfo(t *testing.T) {
 	lookup := createConsistentLookup(t, "consistent_lookup")
-	if lookup.Cost() != 20 {
-		t.Errorf("Cost(): %d, want 20", lookup.Cost())
-	}
-	if strings.Compare("consistent_lookup", lookup.String()) != 0 {
-		t.Errorf("String(): %s, want consistent_lookup", lookup.String())
-	}
-	if lookup.IsUnique() {
-		t.Errorf("IsUnique(): %v, want false", lookup.IsUnique())
-	}
+	assert.Equal(t, 20, lookup.Cost())
+	assert.Equal(t, "consistent_lookup", lookup.String())
+	assert.False(t, lookup.IsUnique())
+	assert.True(t, lookup.NeedsVCursor())
 }
 
 func TestConsistentLookupUniqueInfo(t *testing.T) {
 	lookup := createConsistentLookup(t, "consistent_lookup_unique")
-	if lookup.Cost() != 10 {
-		t.Errorf("Cost(): %d, want 10", lookup.Cost())
-	}
-	if strings.Compare("consistent_lookup_unique", lookup.String()) != 0 {
-		t.Errorf("String(): %s, want consistent_lookup_unique", lookup.String())
-	}
-	if !lookup.IsUnique() {
-		t.Errorf("IsUnique(): %v, want true", lookup.IsUnique())
-	}
+	assert.Equal(t, 10, lookup.Cost())
+	assert.Equal(t, "consistent_lookup_unique", lookup.String())
+	assert.True(t, lookup.IsUnique())
+	assert.True(t, lookup.NeedsVCursor())
 }
 
 func TestConsistentLookupMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/hash_test.go b/go/vt/vtgate/vindexes/hash_test.go
index 847cf1968f..9550ce9535 100644
--- a/go/vt/vtgate/vindexes/hash_test.go
+++ b/go/vt/vtgate/vindexes/hash_test.go
@@ -18,9 +18,9 @@ package vindexes
 
 import (
 	"reflect"
-	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -35,16 +35,11 @@ func init() {
 	hash = hv.(SingleColumn)
 }
 
-func TestHashCost(t *testing.T) {
-	if hash.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", hash.Cost())
-	}
-}
-
-func TestHashString(t *testing.T) {
-	if strings.Compare("nn", hash.String()) != 0 {
-		t.Errorf("String(): %s, want hash", hash.String())
-	}
+func TestHashInfo(t *testing.T) {
+	assert.Equal(t, 1, hash.Cost())
+	assert.Equal(t, "nn", hash.String())
+	assert.True(t, hash.IsUnique())
+	assert.False(t, hash.NeedsVCursor())
 }
 
 func TestHashMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/lookup_hash_test.go b/go/vt/vtgate/vindexes/lookup_hash_test.go
index 414c86ca93..076b17d49e 100644
--- a/go/vt/vtgate/vindexes/lookup_hash_test.go
+++ b/go/vt/vtgate/vindexes/lookup_hash_test.go
@@ -18,9 +18,9 @@ package vindexes
 
 import (
 	"reflect"
-	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
@@ -49,28 +49,18 @@ func TestLookupHashNew(t *testing.T) {
 	}
 }
 
-func TestLookupHashCost(t *testing.T) {
+func TestLookupHashInfo(t *testing.T) {
 	lookuphash := createLookup(t, "lookup_hash", false)
-	lookuphashunique := createLookup(t, "lookup_hash_unique", false)
-
-	if lookuphash.Cost() != 20 {
-		t.Errorf("Cost(): %d, want 20", lookuphash.Cost())
-	}
-	if lookuphashunique.Cost() != 10 {
-		t.Errorf("Cost(): %d, want 10", lookuphashunique.Cost())
-	}
-}
+	assert.Equal(t, 20, lookuphash.Cost())
+	assert.Equal(t, "lookup_hash", lookuphash.String())
+	assert.False(t, lookuphash.IsUnique())
+	assert.True(t, lookuphash.NeedsVCursor())
 
-func TestLookupHashString(t *testing.T) {
-	lookuphash := createLookup(t, "lookup_hash", false)
 	lookuphashunique := createLookup(t, "lookup_hash_unique", false)
-
-	if strings.Compare("lookup_hash", lookuphash.String()) != 0 {
-		t.Errorf("String(): %s, want lookup_hash", lookuphash.String())
-	}
-	if strings.Compare("lookup_hash_unique", lookuphashunique.String()) != 0 {
-		t.Errorf("String(): %s, want lookup_hash_unique", lookuphashunique.String())
-	}
+	assert.Equal(t, 10, lookuphashunique.Cost())
+	assert.Equal(t, "lookup_hash_unique", lookuphashunique.String())
+	assert.True(t, lookuphashunique.IsUnique())
+	assert.True(t, lookuphashunique.NeedsVCursor())
 }
 
 func TestLookupHashMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/lookup_hash_unique_test.go b/go/vt/vtgate/vindexes/lookup_hash_unique_test.go
index 27ea2cb4e2..445f12fde7 100644
--- a/go/vt/vtgate/vindexes/lookup_hash_unique_test.go
+++ b/go/vt/vtgate/vindexes/lookup_hash_unique_test.go
@@ -20,6 +20,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
@@ -54,11 +55,12 @@ func TestLookupHashUniqueNew(t *testing.T) {
 	}
 }
 
-func TestLookupHashUniqueCost(t *testing.T) {
+func TestLookupHashUniqueInfo(t *testing.T) {
 	lhu := createLookup(t, "lookup_hash_unique", false)
-	if lhu.Cost() != 10 {
-		t.Errorf("Cost(): %d, want 10", lhu.Cost())
-	}
+	assert.Equal(t, 10, lhu.Cost())
+	assert.Equal(t, "lookup_hash_unique", lhu.String())
+	assert.True(t, lhu.IsUnique())
+	assert.True(t, lhu.NeedsVCursor())
 }
 
 func TestLookupHashUniqueMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/lookup_test.go b/go/vt/vtgate/vindexes/lookup_test.go
index 12865b8fb7..5393af3758 100644
--- a/go/vt/vtgate/vindexes/lookup_test.go
+++ b/go/vt/vtgate/vindexes/lookup_test.go
@@ -23,6 +23,7 @@ import (
 
 	"strings"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 
 	"vitess.io/vitess/go/vt/key"
@@ -113,18 +114,12 @@ func TestLookupNonUniqueNew(t *testing.T) {
 	}
 }
 
-func TestLookupNonUniqueCost(t *testing.T) {
+func TestLookupNonUniqueInfo(t *testing.T) {
 	lookupNonUnique := createLookup(t, "lookup", false)
-	if lookupNonUnique.Cost() != 20 {
-		t.Errorf("Cost(): %d, want 20", lookupNonUnique.Cost())
-	}
-}
-
-func TestLookupNonUniqueString(t *testing.T) {
-	lookupNonUnique := createLookup(t, "lookup", false)
-	if strings.Compare("lookup", lookupNonUnique.String()) != 0 {
-		t.Errorf("String(): %s, want lookup", lookupNonUnique.String())
-	}
+	assert.Equal(t, 20, lookupNonUnique.Cost())
+	assert.Equal(t, "lookup", lookupNonUnique.String())
+	assert.False(t, lookupNonUnique.IsUnique())
+	assert.True(t, lookupNonUnique.NeedsVCursor())
 }
 
 func TestLookupNilVCursor(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/lookup_unique_test.go b/go/vt/vtgate/vindexes/lookup_unique_test.go
index cb9ab2bed8..9165bab6c7 100644
--- a/go/vt/vtgate/vindexes/lookup_unique_test.go
+++ b/go/vt/vtgate/vindexes/lookup_unique_test.go
@@ -18,9 +18,9 @@ package vindexes
 
 import (
 	"reflect"
-	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 	querypb "vitess.io/vitess/go/vt/proto/query"
@@ -56,18 +56,11 @@ func TestLookupUniqueNew(t *testing.T) {
 	}
 }
 
-func TestLookupUniqueCost(t *testing.T) {
+func TestLookupUniqueInfo(t *testing.T) {
 	lookupUnique := createLookup(t, "lookup_unique", false)
-	if lookupUnique.Cost() != 10 {
-		t.Errorf("Cost(): %d, want 10", lookupUnique.Cost())
-	}
-}
-
-func TestLookupUniqueString(t *testing.T) {
-	lookupUnique := createLookup(t, "lookup_unique", false)
-	if strings.Compare("lookup_unique", lookupUnique.String()) != 0 {
-		t.Errorf("String(): %s, want lookup_unique", lookupUnique.String())
-	}
+	assert.Equal(t, 10, lookupUnique.Cost())
+	assert.Equal(t, "lookup_unique", lookupUnique.String())
+	assert.True(t, lookupUnique.IsUnique())
 }
 
 func TestLookupUniqueMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/null_test.go b/go/vt/vtgate/vindexes/null_test.go
index 11568f9eb5..8210c3ae80 100644
--- a/go/vt/vtgate/vindexes/null_test.go
+++ b/go/vt/vtgate/vindexes/null_test.go
@@ -18,9 +18,9 @@ package vindexes
 
 import (
 	"reflect"
-	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -35,16 +35,11 @@ func init() {
 	null = hv.(SingleColumn)
 }
 
-func TestNullCost(t *testing.T) {
-	if null.Cost() != 0 {
-		t.Errorf("Cost(): %d, want 0", null.Cost())
-	}
-}
-
-func TestNullString(t *testing.T) {
-	if strings.Compare("nn", null.String()) != 0 {
-		t.Errorf("String(): %s, want null", null.String())
-	}
+func TestNullInfo(t *testing.T) {
+	assert.Equal(t, 100, null.Cost())
+	assert.Equal(t, "nn", null.String())
+	assert.True(t, null.IsUnique())
+	assert.False(t, null.NeedsVCursor())
 }
 
 func TestNullMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/numeric_static_map_test.go b/go/vt/vtgate/vindexes/numeric_static_map_test.go
index 94035320f0..bd54cef1c6 100644
--- a/go/vt/vtgate/vindexes/numeric_static_map_test.go
+++ b/go/vt/vtgate/vindexes/numeric_static_map_test.go
@@ -20,8 +20,8 @@ import (
 	"reflect"
 	"testing"
 
-	"strings"
-
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -38,24 +38,13 @@ func createVindex() (SingleColumn, error) {
 	return vindex.(SingleColumn), nil
 }
 
-func TestNumericStaticMapCost(t *testing.T) {
-	numericStaticMap, err := createVindex()
-	if err != nil {
-		t.Fatalf("failed to create vindex: %v", err)
-	}
-	if numericStaticMap.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", numericStaticMap.Cost())
-	}
-}
-
-func TestNumericStaticMapString(t *testing.T) {
+func TestNumericStaticMapInfo(t *testing.T) {
 	numericStaticMap, err := createVindex()
-	if err != nil {
-		t.Fatalf("failed to create vindex: %v", err)
-	}
-	if strings.Compare("numericStaticMap", numericStaticMap.String()) != 0 {
-		t.Errorf("String(): %s, want num", numericStaticMap.String())
-	}
+	require.NoError(t, err)
+	assert.Equal(t, 1, numericStaticMap.Cost())
+	assert.Equal(t, "numericStaticMap", numericStaticMap.String())
+	assert.True(t, numericStaticMap.IsUnique())
+	assert.False(t, numericStaticMap.NeedsVCursor())
 }
 
 func TestNumericStaticMapMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/numeric_test.go b/go/vt/vtgate/vindexes/numeric_test.go
index 3d0d053241..326d23dcf1 100644
--- a/go/vt/vtgate/vindexes/numeric_test.go
+++ b/go/vt/vtgate/vindexes/numeric_test.go
@@ -20,8 +20,7 @@ import (
 	"reflect"
 	"testing"
 
-	"strings"
-
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -33,16 +32,11 @@ func init() {
 	numeric = vindex.(SingleColumn)
 }
 
-func TestNumericCost(t *testing.T) {
-	if numeric.Cost() != 0 {
-		t.Errorf("Cost(): %d, want 0", numeric.Cost())
-	}
-}
-
-func TestNumericString(t *testing.T) {
-	if strings.Compare("num", numeric.String()) != 0 {
-		t.Errorf("String(): %s, want num", numeric.String())
-	}
+func TestNumericInfo(t *testing.T) {
+	assert.Equal(t, 0, numeric.Cost())
+	assert.Equal(t, "num", numeric.String())
+	assert.True(t, numeric.IsUnique())
+	assert.False(t, numeric.NeedsVCursor())
 }
 
 func TestNumericMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/region_experimental_test.go b/go/vt/vtgate/vindexes/region_experimental_test.go
index f1162f4361..c0b0b761ef 100644
--- a/go/vt/vtgate/vindexes/region_experimental_test.go
+++ b/go/vt/vtgate/vindexes/region_experimental_test.go
@@ -21,16 +21,18 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
 
 func TestRegionExperimentalMisc(t *testing.T) {
 	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 	assert.Equal(t, 1, ge.Cost())
 	assert.Equal(t, "region_experimental", ge.String())
 	assert.True(t, ge.IsUnique())
+	assert.False(t, ge.NeedsVCursor())
 }
 
 func TestRegionExperimentalMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/reverse_bits_test.go b/go/vt/vtgate/vindexes/reverse_bits_test.go
index a135b8ca56..4cc12cbb96 100644
--- a/go/vt/vtgate/vindexes/reverse_bits_test.go
+++ b/go/vt/vtgate/vindexes/reverse_bits_test.go
@@ -18,9 +18,9 @@ package vindexes
 
 import (
 	"reflect"
-	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -35,16 +35,11 @@ func init() {
 	reverseBits = hv.(SingleColumn)
 }
 
-func TestReverseBitsCost(t *testing.T) {
-	if reverseBits.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", reverseBits.Cost())
-	}
-}
-
-func TestReverseBitsString(t *testing.T) {
-	if strings.Compare("rr", reverseBits.String()) != 0 {
-		t.Errorf("String(): %s, want hash", reverseBits.String())
-	}
+func TestReverseBitsInfo(t *testing.T) {
+	assert.Equal(t, 1, reverseBits.Cost())
+	assert.Equal(t, "rr", reverseBits.String())
+	assert.True(t, reverseBits.IsUnique())
+	assert.False(t, reverseBits.NeedsVCursor())
 }
 
 func TestReverseBitsMap(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/unicodeloosemd5_test.go b/go/vt/vtgate/vindexes/unicodeloosemd5_test.go
index 89bcc19fbb..04695d2e34 100644
--- a/go/vt/vtgate/vindexes/unicodeloosemd5_test.go
+++ b/go/vt/vtgate/vindexes/unicodeloosemd5_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
 )
@@ -33,16 +34,11 @@ func init() {
 	charVindex = vindex.(SingleColumn)
 }
 
-func TestUnicodeLooseMD5Cost(t *testing.T) {
-	if charVindex.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", charVindex.Cost())
-	}
-}
-
-func TestUnicodeLooseMD5String(t *testing.T) {
-	if strings.Compare("utf8ch", charVindex.String()) != 0 {
-		t.Errorf("String(): %s, want utf8ch", charVindex.String())
-	}
+func TestUnicodeLooseMD5Info(t *testing.T) {
+	assert.Equal(t, 1, charVindex.Cost())
+	assert.Equal(t, "utf8ch", charVindex.String())
+	assert.True(t, charVindex.IsUnique())
+	assert.False(t, charVindex.NeedsVCursor())
 }
 
 func TestUnicodeLooseMD5Map(t *testing.T) {
diff --git a/go/vt/vtgate/vindexes/vschema_test.go b/go/vt/vtgate/vindexes/vschema_test.go
index 5ed01c5285..3c18234f86 100644
--- a/go/vt/vtgate/vindexes/vschema_test.go
+++ b/go/vt/vtgate/vindexes/vschema_test.go
@@ -45,6 +45,7 @@ type stFU struct {
 func (v *stFU) String() string                                                    { return v.name }
 func (*stFU) Cost() int                                                           { return 1 }
 func (*stFU) IsUnique() bool                                                      { return true }
+func (*stFU) NeedsVCursor() bool                                                  { return false }
 func (*stFU) Verify(VCursor, []sqltypes.Value, [][]byte) ([]bool, error)          { return []bool{}, nil }
 func (*stFU) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) { return nil, nil }
 
@@ -63,6 +64,7 @@ type stLN struct {
 func (v *stLN) String() string                                                    { return v.name }
 func (*stLN) Cost() int                                                           { return 0 }
 func (*stLN) IsUnique() bool                                                      { return false }
+func (*stLN) NeedsVCursor() bool                                                  { return false }
 func (*stLN) Verify(VCursor, []sqltypes.Value, [][]byte) ([]bool, error)          { return []bool{}, nil }
 func (*stLN) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) { return nil, nil }
 func (*stLN) Create(VCursor, [][]sqltypes.Value, [][]byte, bool) error            { return nil }
@@ -85,6 +87,7 @@ type stLU struct {
 func (v *stLU) String() string                                                    { return v.name }
 func (*stLU) Cost() int                                                           { return 2 }
 func (*stLU) IsUnique() bool                                                      { return true }
+func (*stLU) NeedsVCursor() bool                                                  { return false }
 func (*stLU) Verify(VCursor, []sqltypes.Value, [][]byte) ([]bool, error)          { return []bool{}, nil }
 func (*stLU) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) { return nil, nil }
 func (*stLU) Create(VCursor, [][]sqltypes.Value, [][]byte, bool) error            { return nil }
@@ -110,6 +113,7 @@ type stLO struct {
 func (v *stLO) String() string                                                    { return v.name }
 func (*stLO) Cost() int                                                           { return 2 }
 func (*stLO) IsUnique() bool                                                      { return true }
+func (*stLO) NeedsVCursor() bool                                                  { return false }
 func (*stLO) Verify(VCursor, []sqltypes.Value, [][]byte) ([]bool, error)          { return []bool{}, nil }
 func (*stLO) Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) { return nil, nil }
 func (*stLO) Create(VCursor, [][]sqltypes.Value, [][]byte, bool) error            { return nil }
diff --git a/go/vt/vtgate/vindexes/xxhash_test.go b/go/vt/vtgate/vindexes/xxhash_test.go
index 187682f972..dc66aa3d7b 100644
--- a/go/vt/vtgate/vindexes/xxhash_test.go
+++ b/go/vt/vtgate/vindexes/xxhash_test.go
@@ -20,10 +20,10 @@ import (
 	"bytes"
 	"fmt"
 	"reflect"
-	"strings"
 	"testing"
 
 	"github.com/cespare/xxhash/v2"
+	"github.com/stretchr/testify/assert"
 
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
@@ -39,16 +39,11 @@ func init() {
 	xxHash = hv.(SingleColumn)
 }
 
-func TestXXHashCost(t *testing.T) {
-	if xxHash.Cost() != 1 {
-		t.Errorf("Cost(): %d, want 1", xxHash.Cost())
-	}
-}
-
-func TestXXHashString(t *testing.T) {
-	if strings.Compare("xxhash_name", xxHash.String()) != 0 {
-		t.Errorf("String(): %s, want xxhash_name", xxHash.String())
-	}
+func TestXXHashInfo(t *testing.T) {
+	assert.Equal(t, 1, xxHash.Cost())
+	assert.Equal(t, "xxhash_name", xxHash.String())
+	assert.True(t, xxHash.IsUnique())
+	assert.False(t, xxHash.NeedsVCursor())
 }
 
 func TestXXHashMap(t *testing.T) {
diff --git a/go/vt/vttablet/tabletserver/vstreamer/local_vschema_test.go b/go/vt/vttablet/tabletserver/vstreamer/local_vschema_test.go
index 0ac313eb77..37ea34dfe4 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/local_vschema_test.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/local_vschema_test.go
@@ -27,78 +27,151 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/vindexes"
 )
 
-var testSrvVSchema = &vschemapb.SrvVSchema{
-	Keyspaces: map[string]*vschemapb.Keyspace{
-		"ks1": {
-			Sharded: true,
-			Vindexes: map[string]*vschemapb.Vindex{
-				"duphash": {
-					Type: "hash",
+func TestFindColVindex(t *testing.T) {
+	testSrvVSchema := &vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"ks1": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"hash": {
+						Type: "hash",
+					},
+					"lookup_unique": {
+						Type: "lookup_unique",
+						Params: map[string]string{
+							"table": "t",
+							"from":  "fromc",
+							"to":    "toc",
+						},
+					},
+					"lookup": {
+						Type: "lookup",
+						Params: map[string]string{
+							"table": "t",
+							"from":  "fromc",
+							"to":    "toc",
+						},
+					},
+					"numeric": {
+						Type: "numeric",
+					},
 				},
-			},
-			Tables: map[string]*vschemapb.Table{
-				"t1": {
-					ColumnVindexes: []*vschemapb.ColumnVindex{{
-						Name:    "duphash",
-						Columns: []string{"id"},
-					}},
+				Tables: map[string]*vschemapb.Table{
+					"t1": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "hash",
+							Columns: []string{"id"},
+						}},
+					},
+					"nogoodvindex1": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "lookup_unique",
+							Columns: []string{"id"},
+						}},
+					},
+					"nogoodvindex2": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "lookup_unique",
+							Columns: []string{"id"},
+						}, {
+							Name:    "lookup",
+							Columns: []string{"id"},
+						}},
+					},
+					"cheapest": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "hash",
+							Columns: []string{"id"},
+						}, {
+							Name:    "numeric",
+							Columns: []string{"id"},
+						}},
+					},
 				},
 			},
-		},
-		"ks2": {
-			Sharded: true,
-			Vindexes: map[string]*vschemapb.Vindex{
-				"duphash": {
-					Type: "hash",
-				},
-				"otherhash": {
-					Type: "hash",
+			"unsharded": {
+				Tables: map[string]*vschemapb.Table{
+					"t1": {},
 				},
 			},
 		},
-	},
-}
-
-func TestFindTable(t *testing.T) {
+	}
 	vschema, err := vindexes.BuildVSchema(testSrvVSchema)
 	require.NoError(t, err)
 
 	testcases := []struct {
-		keyspace  string
-		tablename string
-		err       string
+		keyspace   string
+		tablename  string
+		vindexname string
+		err        string
 	}{{
+		keyspace:   "ks1",
+		tablename:  "t1",
+		vindexname: "hash",
+	}, {
 		keyspace:  "ks1",
-		tablename: "t1",
-		err:       "",
+		tablename: "nogoodvindex1",
+		err:       "could not find a vindex to compute keyspace id for table nogoodvindex1",
 	}, {
 		keyspace:  "ks1",
-		tablename: "t2",
-		err:       "table t2 not found",
+		tablename: "nogoodvindex2",
+		err:       "could not find a vindex to compute keyspace id for table nogoodvindex2",
 	}, {
-		keyspace:  "noks",
-		tablename: "t2",
-		err:       "keyspace noks not found in vschema",
+		keyspace:   "ks1",
+		tablename:  "cheapest",
+		vindexname: "numeric",
+	}, {
+		keyspace:  "unsharded",
+		tablename: "t1",
+		err:       "table t1 has no vindex",
 	}}
 	for _, tcase := range testcases {
 		lvs := &localVSchema{
 			keyspace: tcase.keyspace,
 			vschema:  vschema,
 		}
-		table, err := lvs.FindTable(tcase.tablename)
+		cv, err := lvs.FindColVindex(tcase.tablename)
 		if err != nil {
-			assert.EqualError(t, err, tcase.err, tcase.keyspace, tcase.tablename)
+			assert.EqualError(t, err, tcase.err, tcase.tablename)
 			continue
 		}
-		assert.NoError(t, err, tcase.keyspace, tcase.tablename)
-		assert.Equal(t, table.Name.String(), tcase.tablename, tcase.keyspace, tcase.tablename)
+		assert.NoError(t, err, tcase.tablename)
+		assert.Equal(t, cv.Name, tcase.vindexname, tcase.tablename)
 	}
 }
 
 func TestFindOrCreateVindex(t *testing.T) {
+	testSrvVSchema := &vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"ks1": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"duphash": {
+						Type: "hash",
+					},
+				},
+			},
+			"ks2": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"duphash": {
+						Type: "hash",
+					},
+					"otherhash": {
+						Type: "hash",
+					},
+				},
+			},
+		},
+	}
 	vschema, err := vindexes.BuildVSchema(testSrvVSchema)
 	require.NoError(t, err)
 
+	lvs := &localVSchema{
+		keyspace: "ks1",
+		vschema:  vschema,
+	}
+
 	testcases := []struct {
 		name string
 		err  string
@@ -125,10 +198,6 @@ func TestFindOrCreateVindex(t *testing.T) {
 		err:  `vindexType "none" not found`,
 	}}
 	for _, tcase := range testcases {
-		lvs := &localVSchema{
-			keyspace: "ks1",
-			vschema:  vschema,
-		}
 		vindex, err := lvs.FindOrCreateVindex(tcase.name)
 		if err != nil {
 			assert.EqualError(t, err, tcase.err, tcase.name)
@@ -140,3 +209,48 @@ func TestFindOrCreateVindex(t *testing.T) {
 		assert.Equal(t, vindex.String(), want, tcase.name)
 	}
 }
+
+func TestFindTable(t *testing.T) {
+	testSrvVSchema := &vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"ks1": {
+				Tables: map[string]*vschemapb.Table{
+					"t1": {},
+				},
+			},
+		},
+	}
+	vschema, err := vindexes.BuildVSchema(testSrvVSchema)
+	require.NoError(t, err)
+
+	testcases := []struct {
+		keyspace  string
+		tablename string
+		err       string
+	}{{
+		keyspace:  "ks1",
+		tablename: "t1",
+		err:       "",
+	}, {
+		keyspace:  "ks1",
+		tablename: "t2",
+		err:       "table t2 not found",
+	}, {
+		keyspace:  "noks",
+		tablename: "t2",
+		err:       "keyspace noks not found in vschema",
+	}}
+	for _, tcase := range testcases {
+		lvs := &localVSchema{
+			keyspace: tcase.keyspace,
+			vschema:  vschema,
+		}
+		table, err := lvs.findTable(tcase.tablename)
+		if err != nil {
+			assert.EqualError(t, err, tcase.err, tcase.keyspace, tcase.tablename)
+			continue
+		}
+		assert.NoError(t, err, tcase.keyspace, tcase.tablename)
+		assert.Equal(t, table.Name.String(), tcase.tablename, tcase.keyspace, tcase.tablename)
+	}
+}