Repository: vitessio/vitess
Pull request #5390 (2019-11-01 04:19:37)

vindex: region_experimental
Implements #5375 

A new `geo_experimental` vindex has been created to showcase the new capabilities. The vindex combines a region and an id to generate a keyspace_id. At the same time, it saves a lookup entry from id to keyspace_id.

saifalharthi: using `geo` prefix got me confused and made me think that it would be a spatial indexing technique or would require partitioning to spatial objects. I think it should be renamed to something more clear.
morgo: > using `geo` prefix got me confused and made me think that it would be a spatial indexing technique or would require partitioning to spatial objects. I think it should be renamed to something more clear.

I agree :-) If we want to get pedantic, legal jurisdictions are not purely by a geographical scheme. We just think of them like that.
sougou: For now, I'll go with `region_experimental`.

Patch:
diff --git a/go.mod b/go.mod
index e3081b3479..ae3b4d9aee 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,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
@@ -49,8 +48,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/engine/insert.go b/go/vt/vtgate/engine/insert.go
index eab98acba8..bf1de16def 100644
--- a/go/vt/vtgate/engine/insert.go
+++ b/go/vt/vtgate/engine/insert.go
@@ -390,34 +390,49 @@ func (ins *Insert) getInsertShardedRoute(vcursor VCursor, bindVars map[string]*q
 	// The output from the following 'process' functions is a list of
 	// keyspace ids. For regular inserts, a failure to find a route
 	// results in an error. For 'ignore' type inserts, the keyspace
-	// id is returned as nil, which is used later to drop such rows.
-	keyspaceIDs, err := ins.processPrimary(vcursor, vindexRowsValues[0], ins.Table.ColumnVindexes[0], bindVars)
+	// id is returned as nil, which is used later to drop the corresponding rows.
+	colVindex := ins.Table.ColumnVindexes[0]
+	keyspaceIDs, err := ins.processPrimary(vcursor, vindexRowsValues[0], colVindex)
 	if err != nil {
 		return nil, nil, vterrors.Wrap(err, "getInsertShardedRoute")
 	}
+	// Primary vindex can be owned. If so, go through the processOwned flow.
+	// If not owned, we don't do processUnowned because there's no need to verify
+	// the keyspace ids we just generated.
+	if colVindex.Owned {
+		if err := ins.processOwned(vcursor, vindexRowsValues[0], colVindex, keyspaceIDs); err != nil {
+			return nil, nil, vterrors.Wrap(err, "getInsertShardedRoute")
+		}
+	}
 
-	for vIdx := 1; vIdx < len(vindexRowsValues); vIdx++ {
+	for vIdx := 1; vIdx < len(ins.Table.ColumnVindexes); vIdx++ {
 		colVindex := ins.Table.ColumnVindexes[vIdx]
 		var err error
 		if colVindex.Owned {
-			switch ins.Opcode {
-			case InsertSharded:
-				err = ins.processOwned(vcursor, vindexRowsValues[vIdx], colVindex, bindVars, keyspaceIDs)
-			case InsertShardedIgnore:
-				// For InsertShardedIgnore, the work is substantially different.
-				// So, we use a separate function.
-				err = ins.processOwnedIgnore(vcursor, vindexRowsValues[vIdx], colVindex, bindVars, keyspaceIDs)
-			default:
-				err = vterrors.Errorf(vtrpcpb.Code_INTERNAL, "BUG: unexpected opcode: %v", ins.Opcode)
-			}
+			err = ins.processOwned(vcursor, vindexRowsValues[vIdx], colVindex, keyspaceIDs)
 		} else {
-			err = ins.processUnowned(vcursor, vindexRowsValues[vIdx], colVindex, bindVars, keyspaceIDs)
+			err = ins.processUnowned(vcursor, vindexRowsValues[vIdx], colVindex, keyspaceIDs)
 		}
 		if err != nil {
 			return nil, nil, vterrors.Wrap(err, "getInsertShardedRoute")
 		}
 	}
 
+	// Build 3-d bindvars. Skip rows with nil keyspace ids in case
+	// we're executing an insert ignore.
+	for vIdx, colVindex := range ins.Table.ColumnVindexes {
+		for rowNum, rowColumnKeys := range vindexRowsValues[vIdx] {
+			if keyspaceIDs[rowNum] == nil {
+				// InsertShardedIgnore: skip the row.
+				continue
+			}
+			for colIdx, vindexKey := range rowColumnKeys {
+				col := colVindex.Columns[colIdx]
+				bindVars[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(vindexKey)
+			}
+		}
+	}
+
 	// We need to know the keyspace ids and the Mids associated with
 	// each RSS.  So we pass the ksid indexes in as ids, and get them back
 	// as values. We also skip nil KeyspaceIds, no need to resolve them.
@@ -465,14 +480,8 @@ func (ins *Insert) getInsertShardedRoute(vcursor VCursor, bindVars map[string]*q
 }
 
 // processPrimary maps the primary vindex values to the keyspace ids.
-func (ins *Insert) processPrimary(vcursor VCursor, vindexKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex, bv map[string]*querypb.BindVariable) ([][]byte, error) {
-	var flattenedVindexKeys []sqltypes.Value
-	// TODO: @rafael - this will change once vindex Primary keys also support multicolumns
-	for _, val := range vindexKeys {
-		flattenedVindexKeys = append(flattenedVindexKeys, val...)
-	}
-
-	destinations, err := colVindex.Vindex.Map(vcursor, flattenedVindexKeys)
+func (ins *Insert) processPrimary(vcursor VCursor, vindexColumnsKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex) ([][]byte, error) {
+	destinations, err := vindexes.Map(colVindex.Vindex, vcursor, vindexColumnsKeys)
 	if err != nil {
 		return nil, err
 	}
@@ -486,56 +495,34 @@ func (ins *Insert) processPrimary(vcursor VCursor, vindexKeys [][]sqltypes.Value
 		case key.DestinationNone:
 			// No valid keyspace id, we may return an error.
 			if ins.Opcode != InsertShardedIgnore {
-				return nil, fmt.Errorf("could not map %v to a keyspace id", flattenedVindexKeys[i])
+				return nil, fmt.Errorf("could not map %v to a keyspace id", vindexColumnsKeys[i])
 			}
 		default:
-			return nil, fmt.Errorf("could not map %v to a unique keyspace id: %v", flattenedVindexKeys[i], destination)
+			return nil, fmt.Errorf("could not map %v to a unique keyspace id: %v", vindexColumnsKeys[i], destination)
 		}
 	}
 
-	for rowNum, vindexKey := range flattenedVindexKeys {
-		if keyspaceIDs[rowNum] == nil {
-			// InsertShardedIgnore: skip the row.
-			continue
-		}
-		for _, col := range colVindex.Columns {
-			bv[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(vindexKey)
-		}
-	}
 	return keyspaceIDs, nil
 }
 
-// processOwned creates vindex entries for the values of an owned column for InsertSharded.
-func (ins *Insert) processOwned(vcursor VCursor, vindexColumnsKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex, bv map[string]*querypb.BindVariable, ksids [][]byte) error {
-	for rowNum, rowColumnKeys := range vindexColumnsKeys {
-		for colIdx, vindexKey := range rowColumnKeys {
-			col := colVindex.Columns[colIdx]
-			bv[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(vindexKey)
-		}
+// processOwned creates vindex entries for the values of an owned column.
+func (ins *Insert) processOwned(vcursor VCursor, vindexColumnsKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex, ksids [][]byte) error {
+	if ins.Opcode == InsertSharded {
+		return colVindex.Vindex.(vindexes.Lookup).Create(vcursor, vindexColumnsKeys, ksids, false /* ignoreMode */)
 	}
-	return colVindex.Vindex.(vindexes.Lookup).Create(vcursor, vindexColumnsKeys, ksids, false /* ignoreMode */)
-}
 
-// processOwnedIgnore creates vindex entries for the values of an owned column for InsertShardedIgnore.
-func (ins *Insert) processOwnedIgnore(vcursor VCursor, vindexColumnsKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex, bv map[string]*querypb.BindVariable, ksids [][]byte) error {
+	// InsertShardedIgnore
 	var createIndexes []int
 	var createKeys [][]sqltypes.Value
 	var createKsids [][]byte
 
 	for rowNum, rowColumnKeys := range vindexColumnsKeys {
-		var rowKeys []sqltypes.Value
 		if ksids[rowNum] == nil {
 			continue
 		}
 		createIndexes = append(createIndexes, rowNum)
+		createKeys = append(createKeys, rowColumnKeys)
 		createKsids = append(createKsids, ksids[rowNum])
-
-		for colIdx, vindexKey := range rowColumnKeys {
-			rowKeys = append(rowKeys, vindexKey)
-			col := colVindex.Columns[colIdx]
-			bv[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(vindexKey)
-		}
-		createKeys = append(createKeys, rowKeys)
 	}
 	if createKeys == nil {
 		return nil
@@ -547,12 +534,7 @@ func (ins *Insert) processOwnedIgnore(vcursor VCursor, vindexColumnsKeys [][]sql
 	}
 	// After creation, verify that the keys map to the keyspace ids. If not, remove
 	// those that don't map.
-	// If values were supplied, we validate against keyspace id.
-	var ids []sqltypes.Value
-	for _, vindexValues := range createKeys {
-		ids = append(ids, vindexValues[0])
-	}
-	verified, err := colVindex.Vindex.Verify(vcursor, ids, createKsids)
+	verified, err := vindexes.Verify(colVindex.Vindex, vcursor, createKeys, createKsids)
 	if err != nil {
 		return err
 	}
@@ -565,26 +547,26 @@ func (ins *Insert) processOwnedIgnore(vcursor VCursor, vindexColumnsKeys [][]sql
 }
 
 // processUnowned either reverse maps or validates the values for an unowned column.
-func (ins *Insert) processUnowned(vcursor VCursor, vindexColumnsKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex, bv map[string]*querypb.BindVariable, ksids [][]byte) error {
+func (ins *Insert) processUnowned(vcursor VCursor, vindexColumnsKeys [][]sqltypes.Value, colVindex *vindexes.ColumnVindex, ksids [][]byte) error {
 	var reverseIndexes []int
 	var reverseKsids [][]byte
 	var verifyIndexes []int
-	var verifyKeys []sqltypes.Value
+	var verifyKeys [][]sqltypes.Value
 	var verifyKsids [][]byte
 
 	for rowNum, rowColumnKeys := range vindexColumnsKeys {
 		// Right now, we only validate against the first column of a colvindex.
-		// TODO(sougou): address this when we add multicolumn Map support.
-		vindexKey := rowColumnKeys[0]
 		if ksids[rowNum] == nil {
 			continue
 		}
-		if vindexKey.IsNull() {
+		// Perform reverse map only for non-multi-column vindexes.
+		_, isMulti := colVindex.Vindex.(vindexes.MultiColumn)
+		if rowColumnKeys[0].IsNull() && !isMulti {
 			reverseIndexes = append(reverseIndexes, rowNum)
 			reverseKsids = append(reverseKsids, ksids[rowNum])
 		} else {
 			verifyIndexes = append(verifyIndexes, rowNum)
-			verifyKeys = append(verifyKeys, vindexKey)
+			verifyKeys = append(verifyKeys, rowColumnKeys)
 			verifyKsids = append(verifyKsids, ksids[rowNum])
 		}
 	}
@@ -601,38 +583,26 @@ func (ins *Insert) processUnowned(vcursor VCursor, vindexColumnsKeys [][]sqltype
 			return err
 		}
 		for i, reverseKey := range reverseKeys {
-			rowNum := reverseIndexes[i]
-			for colIdx, col := range colVindex.Columns {
-				if colIdx == 0 {
-					// Fill the first column with the reverse-mapped value.
-					bv[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(reverseKey)
-				} else {
-					// Fill other columns with supplied values.
-					bv[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(vindexColumnsKeys[rowNum][colIdx])
-				}
-			}
+			// Fill the first column with the reverse-mapped value.
+			vindexColumnsKeys[reverseIndexes[i]][0] = reverseKey
 		}
 	}
 
 	if verifyKsids != nil {
 		// If values were supplied, we validate against keyspace id.
-		verified, err := colVindex.Vindex.Verify(vcursor, verifyKeys, verifyKsids)
+		verified, err := vindexes.Verify(colVindex.Vindex, vcursor, verifyKeys, verifyKsids)
 		if err != nil {
 			return err
 		}
 		for i, v := range verified {
-			rowNum := verifyIndexes[i]
 			if !v {
 				if ins.Opcode != InsertShardedIgnore {
 					return fmt.Errorf("values %v for column %v does not map to keyspace ids", vindexColumnsKeys, colVindex.Columns)
 				}
 				// InsertShardedIgnore: skip the row.
-				ksids[rowNum] = nil
+				ksids[verifyIndexes[i]] = nil
 				continue
 			}
-			for colIdx, col := range colVindex.Columns {
-				bv[insertVarName(col, rowNum)] = sqltypes.ValueBindVariable(vindexColumnsKeys[rowNum][colIdx])
-			}
 		}
 	}
 	return nil
diff --git a/go/vt/vtgate/vindexes/binary.go b/go/vt/vtgate/vindexes/binary.go
index e26e9fdaa8..1d9e2bf132 100644
--- a/go/vt/vtgate/vindexes/binary.go
+++ b/go/vt/vtgate/vindexes/binary.go
@@ -54,11 +54,6 @@ func (vind *Binary) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *Binary) IsFunctional() bool {
-	return true
-}
-
 // 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 288195d912..a75326e276 100644
--- a/go/vt/vtgate/vindexes/binarymd5.go
+++ b/go/vt/vtgate/vindexes/binarymd5.go
@@ -53,11 +53,6 @@ func (vind *BinaryMD5) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *BinaryMD5) IsFunctional() bool {
-	return true
-}
-
 // 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 0bb3291981..546b70c244 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup.go
@@ -199,11 +199,6 @@ func (lu *clCommon) String() string {
 	return lu.name
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (lu *clCommon) IsFunctional() bool {
-	return false
-}
-
 // Verify returns true if ids maps to ksids.
 func (lu *clCommon) Verify(vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	return lu.lkp.VerifyCustom(vcursor, ids, ksidsToValues(ksids), vtgate.CommitOrder_PRE)
diff --git a/go/vt/vtgate/vindexes/hash.go b/go/vt/vtgate/vindexes/hash.go
index b7e625d4c5..43859e4f38 100644
--- a/go/vt/vtgate/vindexes/hash.go
+++ b/go/vt/vtgate/vindexes/hash.go
@@ -62,11 +62,6 @@ func (vind *Hash) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *Hash) IsFunctional() bool {
-	return true
-}
-
 // 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 daadc2eae5..a6869066e4 100644
--- a/go/vt/vtgate/vindexes/lookup.go
+++ b/go/vt/vtgate/vindexes/lookup.go
@@ -61,11 +61,6 @@ func (ln *LookupNonUnique) IsUnique() bool {
 	return false
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (ln *LookupNonUnique) IsFunctional() bool {
-	return false
-}
-
 // 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))
@@ -216,11 +211,6 @@ func (lu *LookupUnique) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (lu *LookupUnique) IsFunctional() bool {
-	return false
-}
-
 // 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 3772b889b0..30118bd274 100644
--- a/go/vt/vtgate/vindexes/lookup_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_hash.go
@@ -93,11 +93,6 @@ func (lh *LookupHash) IsUnique() bool {
 	return false
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (lh *LookupHash) IsFunctional() bool {
-	return false
-}
-
 // 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))
@@ -249,11 +244,6 @@ func (lhu *LookupHashUnique) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (lhu *LookupHashUnique) IsFunctional() bool {
-	return false
-}
-
 // 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_internal.go b/go/vt/vtgate/vindexes/lookup_internal.go
index 211ffc933c..bfcc38a2c1 100644
--- a/go/vt/vtgate/vindexes/lookup_internal.go
+++ b/go/vt/vtgate/vindexes/lookup_internal.go
@@ -62,6 +62,9 @@ func (lkp *lookupInternal) Init(lookupQueryParams map[string]string, autocommit,
 
 // Lookup performs a lookup for the ids.
 func (lkp *lookupInternal) Lookup(vcursor VCursor, ids []sqltypes.Value) ([]*sqltypes.Result, error) {
+	if vcursor == nil {
+		return nil, fmt.Errorf("cannot perform lookup: no vcursor provided")
+	}
 	results := make([]*sqltypes.Result, 0, len(ids))
 	for _, id := range ids {
 		bindVars := map[string]*querypb.BindVariable{
diff --git a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
index b55fcb88fc..30b12a33f3 100644
--- a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
@@ -94,11 +94,6 @@ func (lh *LookupUnicodeLooseMD5Hash) IsUnique() bool {
 	return false
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (lh *LookupUnicodeLooseMD5Hash) IsFunctional() bool {
-	return false
-}
-
 // 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))
@@ -261,11 +256,6 @@ func (lhu *LookupUnicodeLooseMD5HashUnique) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns false since the Vindex is not functional.
-func (lhu *LookupUnicodeLooseMD5HashUnique) IsFunctional() bool {
-	return false
-}
-
 // 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 36ed1c42f5..d3fc3fc56b 100644
--- a/go/vt/vtgate/vindexes/null.go
+++ b/go/vt/vtgate/vindexes/null.go
@@ -58,11 +58,6 @@ func (vind *Null) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *Null) IsFunctional() bool {
-	return true
-}
-
 // 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 34f0648a28..1b0b5c2a07 100644
--- a/go/vt/vtgate/vindexes/numeric.go
+++ b/go/vt/vtgate/vindexes/numeric.go
@@ -57,11 +57,6 @@ func (*Numeric) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (*Numeric) IsFunctional() bool {
-	return true
-}
-
 // 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 fa67794792..9f51a80a88 100644
--- a/go/vt/vtgate/vindexes/numeric_static_map.go
+++ b/go/vt/vtgate/vindexes/numeric_static_map.go
@@ -80,11 +80,6 @@ func (vind *NumericStaticMap) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *NumericStaticMap) IsFunctional() bool {
-	return true
-}
-
 // 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
new file mode 100644
index 0000000000..15159517dd
--- /dev/null
+++ b/go/vt/vtgate/vindexes/region_experimental.go
@@ -0,0 +1,135 @@
+/*
+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 vindexes
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+
+	"vitess.io/vitess/go/sqltypes"
+	"vitess.io/vitess/go/vt/key"
+)
+
+var (
+	_ Vindex        = (*RegionExperimental)(nil)
+	_ Lookup        = (*RegionExperimental)(nil)
+	_ WantOwnerInfo = (*RegionExperimental)(nil)
+	_ MultiColumn   = (*RegionExperimental)(nil)
+)
+
+func init() {
+	Register("region_experimental", NewRegionExperimental)
+}
+
+// RegionExperimental defines a vindex that uses a lookup table.
+// The table is expected to define the id column as unique. It's
+// Unique and a Lookup.
+type RegionExperimental struct {
+	regionBytes int
+	*ConsistentLookupUnique
+}
+
+// NewRegionExperimental creates a RegionExperimental vindex.
+// The supplied map requires all the fields of "consistent_lookup_unique".
+// Additionally, it requires a region_bytes argument whose value can be "1", or "2".
+func NewRegionExperimental(name string, m map[string]string) (Vindex, error) {
+	rbs, ok := m["region_bytes"]
+	if !ok {
+		return nil, fmt.Errorf("region_experimental missing region_bytes param")
+	}
+	var rb int
+	switch rbs {
+	case "1":
+		rb = 1
+	case "2":
+		rb = 2
+	default:
+		return nil, fmt.Errorf("region_bits must be 1 or 2: %v", rbs)
+	}
+	vindex, err := NewConsistentLookupUnique(name, m)
+	if err != nil {
+		// Unreachable.
+		return nil, err
+	}
+	cl := vindex.(*ConsistentLookupUnique)
+	if len(cl.lkp.FromColumns) != 2 {
+		return nil, fmt.Errorf("two columns are required for region_experimental: %v", cl.lkp.FromColumns)
+	}
+	return &RegionExperimental{
+		regionBytes:            rb,
+		ConsistentLookupUnique: cl,
+	}, nil
+}
+
+// MapMulti satisfies MultiColumn.
+func (ge *RegionExperimental) MapMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
+	destinations := make([]key.Destination, 0, len(rowsColValues))
+	for _, row := range rowsColValues {
+		if len(row) != 2 {
+			destinations = append(destinations, key.DestinationNone{})
+			continue
+		}
+		// Compute hash.
+		hn, err := sqltypes.ToUint64(row[0])
+		if err != nil {
+			destinations = append(destinations, key.DestinationNone{})
+			continue
+		}
+		h := vhash(hn)
+
+		// Compute region prefix.
+		rn, err := sqltypes.ToUint64(row[1])
+		if err != nil {
+			destinations = append(destinations, key.DestinationNone{})
+			continue
+		}
+		r := make([]byte, 2)
+		binary.BigEndian.PutUint16(r, uint16(rn))
+
+		// Concatenate and add to destinations.
+		if ge.regionBytes == 1 {
+			r = r[1:]
+		}
+		dest := append(r, h...)
+		destinations = append(destinations, key.DestinationKeyspaceID(dest))
+	}
+	return destinations, nil
+}
+
+// VerifyMulti satisfies MultiColumn.
+func (ge *RegionExperimental) VerifyMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error) {
+	result := make([]bool, len(rowsColValues))
+	destinations, _ := ge.MapMulti(vcursor, rowsColValues)
+	for i, dest := range destinations {
+		destksid, ok := dest.(key.DestinationKeyspaceID)
+		if !ok {
+			continue
+		}
+		result[i] = bytes.Equal([]byte(destksid), ksids[i])
+	}
+	// We also need to verify from the lookup.
+	// TODO(sougou): we should only verify true values from previous result.
+	lresult, err := Verify(ge.ConsistentLookupUnique, vcursor, rowsColValues, ksids)
+	if err != nil {
+		return nil, err
+	}
+	for i := range result {
+		result[i] = result[i] && lresult[i]
+	}
+	return result, nil
+}
diff --git a/go/vt/vtgate/vindexes/reverse_bits.go b/go/vt/vtgate/vindexes/reverse_bits.go
index eab8ee3993..9957988fe5 100644
--- a/go/vt/vtgate/vindexes/reverse_bits.go
+++ b/go/vt/vtgate/vindexes/reverse_bits.go
@@ -59,11 +59,6 @@ func (vind *ReverseBits) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *ReverseBits) IsFunctional() bool {
-	return true
-}
-
 // 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 ca1668255c..8264ebfe91 100644
--- a/go/vt/vtgate/vindexes/unicodeloosemd5.go
+++ b/go/vt/vtgate/vindexes/unicodeloosemd5.go
@@ -62,11 +62,6 @@ func (vind *UnicodeLooseMD5) IsUnique() bool {
 	return true
 }
 
-// IsFunctional returns true since the Vindex is functional.
-func (vind *UnicodeLooseMD5) IsFunctional() bool {
-	return true
-}
-
 // 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 66493c4c67..35ab8c853e 100644
--- a/go/vt/vtgate/vindexes/vindex.go
+++ b/go/vt/vtgate/vindexes/vindex.go
@@ -57,16 +57,6 @@ type Vindex interface {
 	// Which means Map() maps to either a KeyRange or a single KeyspaceID.
 	IsUnique() bool
 
-	// IsFunctional returns true if the Vindex can compute
-	// the keyspace id from the id without a lookup.
-	// A Functional vindex is also required to be Unique.
-	// Which means Map() maps to either a KeyRange or a single KeyspaceID.
-	IsFunctional() bool
-
-	// Verify must be implented by all vindexes. It should return
-	// true if the ids can be mapped to the keyspace ids.
-	Verify(cursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error)
-
 	// Map can map ids to key.Destination objects.
 	// If the Vindex is unique, each id would map to either
 	// a KeyRange, or a single KeyspaceID.
@@ -74,7 +64,18 @@ type Vindex interface {
 	// a KeyRange, or a list of KeyspaceID.
 	// If the error returned if nil, then the array len of the
 	// key.Destination array must match len(ids).
-	Map(cursor VCursor, ids []sqltypes.Value) ([]key.Destination, error)
+	Map(vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error)
+
+	// Verify must be implented by all vindexes. It should return
+	// true if the ids can be mapped to the keyspace ids.
+	Verify(vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error)
+}
+
+// MultiColumn defines the interface for vindexes that can
+// support multi-column vindexes.
+type MultiColumn interface {
+	MapMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error)
+	VerifyMulti(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error)
 }
 
 // A Reversible vindex is one that can perform a
@@ -82,7 +83,7 @@ type Vindex interface {
 // is optional. If present, VTGate can use it to
 // fill column values based on the target keyspace id.
 type Reversible interface {
-	ReverseMap(cursor VCursor, ks [][]byte) ([]sqltypes.Value, error)
+	ReverseMap(vcursor VCursor, ks [][]byte) ([]sqltypes.Value, error)
 }
 
 // A Lookup vindex is one that needs to lookup
@@ -138,3 +139,27 @@ func CreateVindex(vindexType, name string, params map[string]string) (Vindex, er
 	}
 	return f(name, params)
 }
+
+// Map invokes MapMulti or Map depending on which is available.
+func Map(vindex Vindex, vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
+	if multi, ok := vindex.(MultiColumn); ok {
+		return multi.MapMulti(vcursor, rowsColValues)
+	}
+	return vindex.Map(vcursor, firstColsOnly(rowsColValues))
+}
+
+// Verify invokes VerifyMulti or Verify depending on which is available.
+func Verify(vindex Vindex, vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error) {
+	if multi, ok := vindex.(MultiColumn); ok {
+		return multi.VerifyMulti(vcursor, rowsColValues, ksids)
+	}
+	return vindex.Verify(vcursor, firstColsOnly(rowsColValues), ksids)
+}
+
+func firstColsOnly(rowsColValues [][]sqltypes.Value) []sqltypes.Value {
+	firstCols := make([]sqltypes.Value, 0, len(rowsColValues))
+	for _, val := range rowsColValues {
+		firstCols = append(firstCols, val[0])
+	}
+	return firstCols
+}
diff --git a/go/vt/vtgate/vindexes/vschema.go b/go/vt/vtgate/vindexes/vschema.go
index fb31f4a21a..8f519fb709 100644
--- a/go/vt/vtgate/vindexes/vschema.go
+++ b/go/vt/vtgate/vindexes/vschema.go
@@ -313,9 +313,6 @@ func buildTables(ks *vschemapb.Keyspace, vschema *VSchema, ksvschema *KeyspaceSc
 				if !columnVindex.Vindex.IsUnique() {
 					return fmt.Errorf("primary vindex %s is not Unique for table %s", ind.Name, tname)
 				}
-				if owned {
-					return fmt.Errorf("primary vindex %s cannot be owned for table %s", ind.Name, tname)
-				}
 			}
 			t.ColumnVindexes = append(t.ColumnVindexes, columnVindex)
 			if owned {
diff --git a/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go b/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
index c620f83d3c..43affd74d3 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/planbuilder.go
@@ -73,7 +73,7 @@ func (plan *Plan) filter(values []sqltypes.Value) (bool, []sqltypes.Value, error
 	result := make([]sqltypes.Value, len(plan.ColExprs))
 	for i, colExpr := range plan.ColExprs {
 		if colExpr.ColNum >= len(values) {
-			return false, nil, fmt.Errorf("index out of range, colExpr.ColNum: %d, len(values):%d", colExpr.ColNum, len(values))
+			return false, nil, fmt.Errorf("index out of range, colExpr.ColNum: %d, len(values): %d", colExpr.ColNum, len(values))
 		}
 		result[i] = values[colExpr.ColNum]
 	}
@@ -360,8 +360,8 @@ func (plan *Plan) analyzeInKeyRange(kschema *vindexes.KeyspaceSchema, exprs sqlp
 		if err != nil {
 			return err
 		}
-		if !plan.Vindex.IsUnique() || !plan.Vindex.IsFunctional() {
-			return fmt.Errorf("vindex must be Unique and Functional to be used for VReplication: %s", vtype)
+		if !plan.Vindex.IsUnique() {
+			return fmt.Errorf("vindex must be Unique to be used for VReplication: %s", vtype)
 		}
 		krExpr = exprs[2]
 	default:


Test patch:
diff --git a/go/vt/vtgate/engine/insert_test.go b/go/vt/vtgate/engine/insert_test.go
index a33900bbf2..d8373863f9 100644
--- a/go/vt/vtgate/engine/insert_test.go
+++ b/go/vt/vtgate/engine/insert_test.go
@@ -20,6 +20,7 @@ import (
 	"errors"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/vtgate/vindexes"
 
@@ -318,7 +319,7 @@ func TestInsertShardedFail(t *testing.T) {
 
 	// The lookup will fail to map to a keyspace id.
 	_, err = ins.Execute(vc, map[string]*querypb.BindVariable{}, false)
-	expectError(t, "Execute", err, "execInsertSharded: getInsertShardedRoute: could not map INT64(1) to a keyspace id")
+	expectError(t, "Execute", err, "execInsertSharded: getInsertShardedRoute: could not map [INT64(1)] to a keyspace id")
 }
 
 func TestInsertShardedGenerate(t *testing.T) {
@@ -644,6 +645,92 @@ func TestInsertShardedOwnedWithNull(t *testing.T) {
 	})
 }
 
+func TestInsertShardedGeo(t *testing.T) {
+	invschema := &vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"sharded": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"geo": {
+						Type: "region_experimental",
+						Params: map[string]string{
+							"region_bytes": "1",
+							"table":        "lkp",
+							"from":         "id,region",
+							"to":           "toc",
+						},
+						Owner: "t1",
+					},
+				},
+				Tables: map[string]*vschemapb.Table{
+					"t1": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "geo",
+							Columns: []string{"id", "region"},
+						}},
+					},
+				},
+			},
+		},
+	}
+	vs, err := vindexes.BuildVSchema(invschema)
+	if err != nil {
+		t.Fatal(err)
+	}
+	ks := vs.Keyspaces["sharded"]
+
+	ins := NewInsert(
+		InsertSharded,
+		ks.Keyspace,
+		[]sqltypes.PlanValue{{
+			// colVindex columns: id, region
+			Values: []sqltypes.PlanValue{{
+				// rows for id
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(1),
+				}, {
+					Value: sqltypes.NewInt64(1),
+				}},
+			}, {
+				// rows for region
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(1),
+				}, {
+					Value: sqltypes.NewInt64(255),
+				}},
+			}},
+		}},
+		ks.Tables["t1"],
+		"prefix",
+		[]string{" mid1", " mid2"},
+		" suffix",
+	)
+
+	vc := &loggingVCursor{
+		shards:       []string{"-20", "20-"},
+		shardForKsid: []string{"20-", "-20"},
+	}
+	_, err = ins.Execute(vc, map[string]*querypb.BindVariable{}, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	vc.ExpectLog(t, []string{
+		// ExecutePre proves that keyspace ids are generated, and that they are inserted into the lookup.
+		`ExecutePre insert into lkp(id, region, toc) values(:id0, :region0, :toc0), (:id1, :region1, :toc1) ` +
+			`id0: type:INT64 value:"1" id1: type:INT64 value:"1" ` +
+			`region0: type:INT64 value:"1" region1: type:INT64 value:"255" ` +
+			`toc0: type:VARBINARY value:"\001\026k@\264J\272K\326" toc1: type:VARBINARY value:"\377\026k@\264J\272K\326"  true`,
+		`ResolveDestinations sharded [value:"0"  value:"1" ] Destinations:DestinationKeyspaceID(01166b40b44aba4bd6),DestinationKeyspaceID(ff166b40b44aba4bd6)`,
+		`ExecuteMultiShard sharded.20-: prefix mid1 suffix /* vtgate:: keyspace_id:01166b40b44aba4bd6 */ ` +
+			`{_id0: type:INT64 value:"1" _id1: type:INT64 value:"1" ` +
+			`_region0: type:INT64 value:"1" _region1: type:INT64 value:"255" } ` +
+			`sharded.-20: prefix mid2 suffix /* vtgate:: keyspace_id:ff166b40b44aba4bd6 */ ` +
+			`{_id0: type:INT64 value:"1" _id1: type:INT64 value:"1" ` +
+			`_region0: type:INT64 value:"1" _region1: type:INT64 value:"255" } ` +
+			`true false`,
+	})
+}
+
 func TestInsertShardedIgnoreOwned(t *testing.T) {
 	invschema := &vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
@@ -822,19 +909,117 @@ func TestInsertShardedIgnoreOwned(t *testing.T) {
 		// Bind vars for rows 2 & 3 may be missing because they were not sent.
 		`ExecuteMultiShard ` +
 			`sharded.20-: prefix mid1 suffix /* vtgate:: keyspace_id:00 */ ` +
-			`{_c10: type:INT64 value:"5" _c12: type:INT64 value:"7" _c13: type:INT64 value:"8" ` +
-			`_c20: type:INT64 value:"9" _c22: type:INT64 value:"11" _c23: type:INT64 value:"12" ` +
+			`{_c10: type:INT64 value:"5" _c13: type:INT64 value:"8" ` +
+			`_c20: type:INT64 value:"9" _c23: type:INT64 value:"12" ` +
 			`_c30: type:INT64 value:"13" _c33: type:INT64 value:"16" ` +
-			`_id0: type:INT64 value:"1" _id2: type:INT64 value:"3" _id3: type:INT64 value:"4" } ` +
+			`_id0: type:INT64 value:"1" _id3: type:INT64 value:"4" } ` +
 			`sharded.-20: prefix mid4 suffix /* vtgate:: keyspace_id:00 */ ` +
-			`{_c10: type:INT64 value:"5" _c12: type:INT64 value:"7" _c13: type:INT64 value:"8" ` +
-			`_c20: type:INT64 value:"9" _c22: type:INT64 value:"11" _c23: type:INT64 value:"12" ` +
+			`{_c10: type:INT64 value:"5" _c13: type:INT64 value:"8" ` +
+			`_c20: type:INT64 value:"9" _c23: type:INT64 value:"12" ` +
 			`_c30: type:INT64 value:"13" _c33: type:INT64 value:"16" ` +
-			`_id0: type:INT64 value:"1" _id2: type:INT64 value:"3" _id3: type:INT64 value:"4" } ` +
+			`_id0: type:INT64 value:"1" _id3: type:INT64 value:"4" } ` +
 			`true false`,
 	})
 }
 
+func TestInsertIgnoreGeo(t *testing.T) {
+	invschema := &vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"sharded": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"geo": {
+						Type: "region_experimental",
+						Params: map[string]string{
+							"region_bytes": "1",
+							"table":        "lkp",
+							"from":         "id,region",
+							"to":           "toc",
+						},
+						Owner: "t1",
+					},
+				},
+				Tables: map[string]*vschemapb.Table{
+					"t1": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "geo",
+							Columns: []string{"id", "region"},
+						}},
+					},
+				},
+			},
+		},
+	}
+	vs, err := vindexes.BuildVSchema(invschema)
+	if err != nil {
+		t.Fatal(err)
+	}
+	ks := vs.Keyspaces["sharded"]
+
+	ins := NewInsert(
+		InsertShardedIgnore,
+		ks.Keyspace,
+		[]sqltypes.PlanValue{{
+			// colVindex columns: id, region
+			Values: []sqltypes.PlanValue{{
+				// rows for id
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(1),
+				}, {
+					Value: sqltypes.NewInt64(2),
+				}},
+			}, {
+				// rows for region
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(1),
+				}, {
+					Value: sqltypes.NewInt64(2),
+				}},
+			}},
+		}},
+		ks.Tables["t1"],
+		"prefix",
+		[]string{" mid1", " mid2"},
+		" suffix",
+	)
+
+	ksid0 := sqltypes.MakeTestResult(
+		sqltypes.MakeTestFields(
+			"to",
+			"varbinary",
+		),
+		"\x00",
+	)
+	noresult := &sqltypes.Result{}
+	vc := &loggingVCursor{
+		shards:       []string{"-20", "20-"},
+		shardForKsid: []string{"20-", "-20"},
+		results: []*sqltypes.Result{
+			// insert lkp
+			noresult,
+			// fail one verification (row 2)
+			ksid0,
+			noresult,
+		},
+	}
+	_, err = ins.Execute(vc, map[string]*querypb.BindVariable{}, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	vc.ExpectLog(t, []string{
+		`ExecutePre insert ignore into lkp(id, region, toc) values(:id0, :region0, :toc0), (:id1, :region1, :toc1) ` +
+			`id0: type:INT64 value:"1" id1: type:INT64 value:"2" ` +
+			`region0: type:INT64 value:"1" region1: type:INT64 value:"2" ` +
+			`toc0: type:VARBINARY value:"\001\026k@\264J\272K\326" toc1: type:VARBINARY value:"\002\006\347\352\"\316\222p\217"  true`,
+		// Row 2 will fail verification. This is what we're testing. The second row should not get inserted.
+		`ExecutePre select id from lkp where id = :id and toc = :toc id: type:INT64 value:"1" toc: type:VARBINARY value:"\001\026k@\264J\272K\326"  false`,
+		`ExecutePre select id from lkp where id = :id and toc = :toc id: type:INT64 value:"2" toc: type:VARBINARY value:"\002\006\347\352\"\316\222p\217"  false`,
+		`ResolveDestinations sharded [value:"0" ] Destinations:DestinationKeyspaceID(01166b40b44aba4bd6)`,
+		`ExecuteMultiShard sharded.20-: prefix mid1 suffix /* vtgate:: keyspace_id:01166b40b44aba4bd6 */ ` +
+			`{_id0: type:INT64 value:"1" _region0: type:INT64 value:"1" } true true`,
+	})
+}
+
 func TestInsertShardedIgnoreOwnedWithNull(t *testing.T) {
 	invschema := &vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
@@ -907,7 +1092,6 @@ func TestInsertShardedIgnoreOwnedWithNull(t *testing.T) {
 		),
 		"\x00",
 	)
-	//noresult := &sqltypes.Result{}
 	vc := &loggingVCursor{
 		shards:       []string{"-20", "20-"},
 		shardForKsid: []string{"-20", "20-"},
@@ -1086,6 +1270,91 @@ func TestInsertShardedUnownedVerify(t *testing.T) {
 	})
 }
 
+func TestInsertUnownedGeo(t *testing.T) {
+	invschema := &vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"sharded": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"primary": {
+						Type: "hash",
+					},
+					"geo": {
+						Type: "region_experimental",
+						Params: map[string]string{
+							"region_bytes": "1",
+							"table":        "lkp",
+							"from":         "other_id,region",
+							"to":           "toc",
+						},
+					},
+				},
+				Tables: map[string]*vschemapb.Table{
+					"t1": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{{
+							Name:    "primary",
+							Columns: []string{"id"},
+						}, {
+							Name:    "geo",
+							Columns: []string{"other_id", "region"},
+						}},
+					},
+				},
+			},
+		},
+	}
+	vs, err := vindexes.BuildVSchema(invschema)
+	if err != nil {
+		t.Fatal(err)
+	}
+	ks := vs.Keyspaces["sharded"]
+
+	ins := NewInsert(
+		InsertSharded,
+		ks.Keyspace,
+		[]sqltypes.PlanValue{{
+			// colVindex columns: id
+			Values: []sqltypes.PlanValue{{
+				// rows for id
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(1),
+				}},
+			}},
+		}, {
+			// colVindex columns: other_id, region
+			Values: []sqltypes.PlanValue{{
+				// rows for other_id
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(2),
+				}},
+			}, {
+				// rows for region
+				Values: []sqltypes.PlanValue{{
+					Value: sqltypes.NewInt64(3),
+				}},
+			}},
+		}},
+		ks.Tables["t1"],
+		"prefix",
+		[]string{" mid1"},
+		" suffix",
+	)
+
+	noresult := &sqltypes.Result{}
+	vc := &loggingVCursor{
+		shards: []string{"-20", "20-"},
+		results: []*sqltypes.Result{
+			// fail verification
+			noresult,
+		},
+	}
+	_, err = ins.Execute(vc, map[string]*querypb.BindVariable{}, false)
+	assert.EqualError(t, err, "execInsertSharded: getInsertShardedRoute: values [[INT64(2) INT64(3)]] for column [other_id region] does not map to keyspace ids")
+	vc.ExpectLog(t, []string{
+		`ExecutePre select other_id from lkp where other_id = :other_id and toc = :toc other_id: type:INT64 value:"2" toc: type:VARBINARY value:"\026k@\264J\272K\326"  false`,
+	})
+}
+
 func TestInsertShardedIgnoreUnownedVerify(t *testing.T) {
 	invschema := &vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
@@ -1192,10 +1461,10 @@ func TestInsertShardedIgnoreUnownedVerify(t *testing.T) {
 		`ExecuteMultiShard ` +
 			`sharded.20-: prefix mid1 suffix /* vtgate:: keyspace_id:166b40b44aba4bd6 */ ` +
 			`{_c30: type:INT64 value:"10" _c32: type:INT64 value:"12" ` +
-			`_id0: type:INT64 value:"1" _id1: type:INT64 value:"2" _id2: type:INT64 value:"3" } ` +
+			`_id0: type:INT64 value:"1" _id2: type:INT64 value:"3" } ` +
 			`sharded.-20: prefix mid3 suffix /* vtgate:: keyspace_id:4eb190c9a2fa169c */ ` +
 			`{_c30: type:INT64 value:"10" _c32: type:INT64 value:"12" ` +
-			`_id0: type:INT64 value:"1" _id1: type:INT64 value:"2" _id2: type:INT64 value:"3" } ` +
+			`_id0: type:INT64 value:"1" _id2: type:INT64 value:"3" } ` +
 			`true false`,
 	})
 }
diff --git a/go/vt/vtgate/engine/vindex_func_test.go b/go/vt/vtgate/engine/vindex_func_test.go
index 2b9e973327..917fc73f1b 100644
--- a/go/vt/vtgate/engine/vindex_func_test.go
+++ b/go/vt/vtgate/engine/vindex_func_test.go
@@ -30,10 +30,9 @@ 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) IsFunctional() bool { return false }
+func (*uvindex) String() string { return "uvindex" }
+func (*uvindex) Cost() int      { return 1 }
+func (*uvindex) IsUnique() bool { return true }
 func (*uvindex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	panic("unimplemented")
 }
@@ -60,10 +59,9 @@ 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) IsFunctional() bool { return false }
+func (*nvindex) String() string { return "nvindex" }
+func (*nvindex) Cost() int      { return 1 }
+func (*nvindex) IsUnique() bool { return false }
 func (*nvindex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	panic("unimplemented")
 }
diff --git a/go/vt/vtgate/executor_dml_test.go b/go/vt/vtgate/executor_dml_test.go
index 551d462566..f492e8e3c0 100644
--- a/go/vt/vtgate/executor_dml_test.go
+++ b/go/vt/vtgate/executor_dml_test.go
@@ -654,7 +654,7 @@ func TestInsertShardedKeyrange(t *testing.T) {
 
 	// If a unique vindex returns a keyrange, we fail the insert
 	_, err := executorExec(executor, "insert into keyrange_table(krcol_unique, krcol) values(1, 1)", nil)
-	want := "execInsertSharded: getInsertShardedRoute: could not map INT64(1) to a unique keyspace id: DestinationKeyRange(-10)"
+	want := "execInsertSharded: getInsertShardedRoute: could not map [INT64(1)] to a unique keyspace id: DestinationKeyRange(-10)"
 	if err == nil || err.Error() != want {
 		t.Errorf("executorExec error: %v, want %s", err, want)
 	}
@@ -786,13 +786,9 @@ func TestInsertShardedIgnore(t *testing.T) {
 		Sql: "insert ignore into insert_ignore_test(pv, owned, verify) values (:_pv0, :_owned0, :_verify0),(:_pv4, :_owned4, :_verify4) /* vtgate:: keyspace_id:166b40b44aba4bd6,166b40b44aba4bd6 */",
 		BindVariables: map[string]*querypb.BindVariable{
 			"_pv0":     sqltypes.Int64BindVariable(1),
-			"_pv2":     sqltypes.Int64BindVariable(3),
-			"_pv3":     sqltypes.Int64BindVariable(4),
 			"_pv4":     sqltypes.Int64BindVariable(5),
 			"_pv5":     sqltypes.Int64BindVariable(6),
 			"_owned0":  sqltypes.Int64BindVariable(1),
-			"_owned2":  sqltypes.Int64BindVariable(3),
-			"_owned3":  sqltypes.Int64BindVariable(4),
 			"_owned4":  sqltypes.Int64BindVariable(5),
 			"_owned5":  sqltypes.Int64BindVariable(6),
 			"_verify0": sqltypes.Int64BindVariable(1),
@@ -807,13 +803,9 @@ func TestInsertShardedIgnore(t *testing.T) {
 		Sql: "insert ignore into insert_ignore_test(pv, owned, verify) values (:_pv5, :_owned5, :_verify5) /* vtgate:: keyspace_id:4eb190c9a2fa169c */",
 		BindVariables: map[string]*querypb.BindVariable{
 			"_pv0":     sqltypes.Int64BindVariable(1),
-			"_pv2":     sqltypes.Int64BindVariable(3),
-			"_pv3":     sqltypes.Int64BindVariable(4),
 			"_pv4":     sqltypes.Int64BindVariable(5),
 			"_pv5":     sqltypes.Int64BindVariable(6),
 			"_owned0":  sqltypes.Int64BindVariable(1),
-			"_owned2":  sqltypes.Int64BindVariable(3),
-			"_owned3":  sqltypes.Int64BindVariable(4),
 			"_owned4":  sqltypes.Int64BindVariable(5),
 			"_owned5":  sqltypes.Int64BindVariable(6),
 			"_verify0": sqltypes.Int64BindVariable(1),
diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go
index 1956738248..1478144e5b 100644
--- a/go/vt/vtgate/executor_framework_test.go
+++ b/go/vt/vtgate/executor_framework_test.go
@@ -282,10 +282,9 @@ 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 (*keyRangeLookuper) IsFunctional() 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) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -307,10 +306,9 @@ 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 (*keyRangeLookuperUnique) IsFunctional() bool { return false }
+func (v *keyRangeLookuperUnique) String() string { return "keyrange_lookuper" }
+func (*keyRangeLookuperUnique) Cost() int        { return 0 }
+func (*keyRangeLookuperUnique) IsUnique() bool   { return true }
 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 4a445b8d31..9e0e72ed30 100644
--- a/go/vt/vtgate/planbuilder/plan_test.go
+++ b/go/vt/vtgate/planbuilder/plan_test.go
@@ -39,10 +39,9 @@ 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 (*hashIndex) IsFunctional() 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) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -57,10 +56,9 @@ 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 (*lookupIndex) IsFunctional() bool { return false }
+func (v *lookupIndex) String() string { return v.name }
+func (*lookupIndex) Cost() int        { return 2 }
+func (*lookupIndex) IsUnique() bool   { return true }
 func (*lookupIndex) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -82,10 +80,9 @@ 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 (*multiIndex) IsFunctional() 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) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -108,10 +105,9 @@ 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 (*costlyIndex) IsFunctional() 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) Verify(vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
diff --git a/go/vt/vtgate/vindexes/consistent_lookup_test.go b/go/vt/vtgate/vindexes/consistent_lookup_test.go
index ee17dd88f8..e1e571d07e 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup_test.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup_test.go
@@ -55,9 +55,6 @@ func TestConsistentLookupInfo(t *testing.T) {
 	if lookup.IsUnique() {
 		t.Errorf("IsUnique(): %v, want false", lookup.IsUnique())
 	}
-	if lookup.IsFunctional() {
-		t.Errorf("IsFunctional(): %v, want false", lookup.IsFunctional())
-	}
 }
 
 func TestConsistentLookupUniqueInfo(t *testing.T) {
@@ -71,9 +68,6 @@ func TestConsistentLookupUniqueInfo(t *testing.T) {
 	if !lookup.IsUnique() {
 		t.Errorf("IsUnique(): %v, want true", lookup.IsUnique())
 	}
-	if lookup.IsFunctional() {
-		t.Errorf("IsFunctional(): %v, want false", lookup.IsFunctional())
-	}
 }
 
 func TestConsistentLookupMap(t *testing.T) {
@@ -499,7 +493,7 @@ func (vc *loggingVCursor) verifyLog(t *testing.T, want []string) {
 			t.Fatalf("index exceeded: %v", vc.log[i:])
 		}
 		if got != want[i] {
-			t.Errorf("log(%d):\n%s, want\n%s", i, got, want[i])
+			t.Errorf("log(%d):\n%q, want\n%q", i, got, want[i])
 		}
 	}
 	if len(want) > len(vc.log) {
diff --git a/go/vt/vtgate/vindexes/lookup_test.go b/go/vt/vtgate/vindexes/lookup_test.go
index 3d89d1d433..ca26ce61e9 100644
--- a/go/vt/vtgate/vindexes/lookup_test.go
+++ b/go/vt/vtgate/vindexes/lookup_test.go
@@ -127,6 +127,16 @@ func TestLookupNonUniqueString(t *testing.T) {
 	}
 }
 
+func TestLookupNilVCursor(t *testing.T) {
+	lookupNonUnique := createLookup(t, "lookup", false)
+
+	_, err := lookupNonUnique.Map(nil, []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)})
+	want := "cannot perform lookup: no vcursor provided"
+	if err == nil || err.Error() != want {
+		t.Errorf("Map(nil vcursor) err: %v, want %v", err, want)
+	}
+}
+
 func TestLookupNonUniqueMap(t *testing.T) {
 	lookupNonUnique := createLookup(t, "lookup", false)
 	vc := &vcursor{numRows: 2}
diff --git a/go/vt/vtgate/vindexes/region_experimental_test.go b/go/vt/vtgate/vindexes/region_experimental_test.go
new file mode 100644
index 0000000000..ef85c65b90
--- /dev/null
+++ b/go/vt/vtgate/vindexes/region_experimental_test.go
@@ -0,0 +1,141 @@
+/*
+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 vindexes
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"vitess.io/vitess/go/sqltypes"
+	"vitess.io/vitess/go/vt/key"
+)
+
+func TestRegionExperimentalMapMulti1(t *testing.T) {
+	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
+	assert.NoError(t, err)
+	got, err := ge.(MultiColumn).MapMulti(nil, [][]sqltypes.Value{{
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}, {
+		sqltypes.NewInt64(1), sqltypes.NewInt64(255),
+	}, {
+		sqltypes.NewInt64(1), sqltypes.NewInt64(256),
+	}, {
+		// Invalid length.
+		sqltypes.NewInt64(1),
+	}, {
+		// Invalid id.
+		sqltypes.NewVarBinary("abcd"), sqltypes.NewInt64(256),
+	}, {
+		// Invalid region.
+		sqltypes.NewInt64(1), sqltypes.NewVarBinary("abcd"),
+	}})
+	assert.NoError(t, err)
+
+	want := []key.Destination{
+		key.DestinationKeyspaceID([]byte("\x01\x16k@\xb4J\xbaK\xd6")),
+		key.DestinationKeyspaceID([]byte("\xff\x16k@\xb4J\xbaK\xd6")),
+		key.DestinationKeyspaceID([]byte("\x00\x16k@\xb4J\xbaK\xd6")),
+		key.DestinationNone{},
+		key.DestinationNone{},
+		key.DestinationNone{},
+	}
+	assert.Equal(t, want, got)
+}
+
+func TestRegionExperimentalMapMulti2(t *testing.T) {
+	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 2)
+	assert.NoError(t, err)
+	got, err := ge.(MultiColumn).MapMulti(nil, [][]sqltypes.Value{{
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}, {
+		sqltypes.NewInt64(1), sqltypes.NewInt64(255),
+	}, {
+		sqltypes.NewInt64(1), sqltypes.NewInt64(256),
+	}, {
+		sqltypes.NewInt64(1), sqltypes.NewInt64(0x10000),
+	}})
+	assert.NoError(t, err)
+
+	want := []key.Destination{
+		key.DestinationKeyspaceID([]byte("\x00\x01\x16k@\xb4J\xbaK\xd6")),
+		key.DestinationKeyspaceID([]byte("\x00\xff\x16k@\xb4J\xbaK\xd6")),
+		key.DestinationKeyspaceID([]byte("\x01\x00\x16k@\xb4J\xbaK\xd6")),
+		key.DestinationKeyspaceID([]byte("\x00\x00\x16k@\xb4J\xbaK\xd6")),
+	}
+	assert.Equal(t, want, got)
+}
+
+func TestRegionExperimentalVerifyMulti(t *testing.T) {
+
+	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
+	assert.NoError(t, err)
+	vals := [][]sqltypes.Value{{
+		// One for match
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}, {
+		// One for mismatch by lookup
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}, {
+		// One for mismatch
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}, {
+		// One invalid value
+		sqltypes.NewInt64(1),
+	}}
+	ksids := [][]byte{
+		[]byte("\x01\x16k@\xb4J\xbaK\xd6"),
+		[]byte("\x01\x16k@\xb4J\xbaK\xd6"),
+		[]byte("no match"),
+		[]byte(""),
+	}
+	vc := &loggingVCursor{}
+	vc.AddResult(makeTestResult(1), nil)
+	// The second value should return a mismatch.
+	vc.AddResult(&sqltypes.Result{}, nil)
+	vc.AddResult(makeTestResult(1), nil)
+	vc.AddResult(makeTestResult(1), nil)
+
+	want := []bool{true, false, false, false}
+	got, err := ge.(MultiColumn).VerifyMulti(vc, vals, ksids)
+	assert.NoError(t, err)
+	vc.verifyLog(t, []string{
+		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc \x01\x16k@\xb4J\xbaK\xd6}] false",
+		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc \x01\x16k@\xb4J\xbaK\xd6}] false",
+		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc no match}] false",
+		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc }] false",
+	})
+	assert.Equal(t, want, got)
+}
+
+func TestRegionExperimentalCreateErrors(t *testing.T) {
+	_, err := createRegionVindex(t, "region_experimental", "f1,f2", 3)
+	assert.EqualError(t, err, "region_bits must be 1 or 2: 3")
+	_, err = CreateVindex("region_experimental", "region_experimental", nil)
+	assert.EqualError(t, err, "region_experimental missing region_bytes param")
+	_, err = createRegionVindex(t, "region_experimental", "f1", 2)
+	assert.EqualError(t, err, "two columns are required for region_experimental: [f1]")
+}
+
+func createRegionVindex(t *testing.T, name, from string, rb int) (Vindex, error) {
+	return CreateVindex(name, name, map[string]string{
+		"region_bytes": strconv.Itoa(rb),
+		"table":        "t",
+		"from":         from,
+		"to":           "toc",
+	})
+}
diff --git a/go/vt/vtgate/vindexes/vindex_test.go b/go/vt/vtgate/vindexes/vindex_test.go
new file mode 100644
index 0000000000..1c85f83a1f
--- /dev/null
+++ b/go/vt/vtgate/vindexes/vindex_test.go
@@ -0,0 +1,85 @@
+/*
+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 vindexes
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"vitess.io/vitess/go/sqltypes"
+	"vitess.io/vitess/go/vt/key"
+)
+
+func TestVindexMap(t *testing.T) {
+	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
+	assert.NoError(t, err)
+
+	got, err := Map(ge, nil, [][]sqltypes.Value{{
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}})
+	assert.NoError(t, err)
+
+	want := []key.Destination{
+		key.DestinationKeyspaceID([]byte("\x01\x16k@\xb4J\xbaK\xd6")),
+	}
+	assert.Equal(t, want, got)
+
+	hash, err := CreateVindex("hash", "hash", nil)
+	assert.NoError(t, err)
+	got, err = Map(hash, nil, [][]sqltypes.Value{{
+		sqltypes.NewInt64(1),
+	}})
+	assert.NoError(t, err)
+	want = []key.Destination{
+		key.DestinationKeyspaceID([]byte("\x16k@\xb4J\xbaK\xd6")),
+	}
+	assert.Equal(t, want, got)
+}
+
+func TestVindexVerify(t *testing.T) {
+	vc := &loggingVCursor{}
+	vc.AddResult(makeTestResult(1), nil)
+	ge, err := createRegionVindex(t, "region_experimental", "f1,f2", 1)
+	assert.NoError(t, err)
+
+	got, err := Verify(ge, vc, [][]sqltypes.Value{{
+		sqltypes.NewInt64(1), sqltypes.NewInt64(1),
+	}},
+		[][]byte{
+			[]byte("\x01\x16k@\xb4J\xbaK\xd6"),
+		},
+	)
+	assert.NoError(t, err)
+	vc.verifyLog(t, []string{
+		"ExecutePre select f1 from t where f1 = :f1 and toc = :toc [{f1 1} {toc \x01\x16k@\xb4J\xbaK\xd6}] false",
+	})
+
+	want := []bool{true}
+	assert.Equal(t, want, got)
+
+	hash, err := CreateVindex("hash", "hash", nil)
+	assert.NoError(t, err)
+	got, err = Verify(hash, nil, [][]sqltypes.Value{{
+		sqltypes.NewInt64(1),
+	}},
+		[][]byte{
+			[]byte("\x16k@\xb4J\xbaK\xd6"),
+		},
+	)
+	assert.NoError(t, err)
+	assert.Equal(t, want, got)
+}
diff --git a/go/vt/vtgate/vindexes/vschema_test.go b/go/vt/vtgate/vindexes/vschema_test.go
index e209919f2a..200636c238 100644
--- a/go/vt/vtgate/vindexes/vschema_test.go
+++ b/go/vt/vtgate/vindexes/vschema_test.go
@@ -45,7 +45,6 @@ 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) IsFunctional() bool                                                  { return true }
 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 }
 
@@ -64,7 +63,6 @@ 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) IsFunctional() 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 }
@@ -87,7 +85,6 @@ 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) IsFunctional() 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 }
@@ -113,7 +110,6 @@ 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) IsFunctional() 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 }
@@ -1558,38 +1554,6 @@ func TestBuildVSchemaNotUniqueFail(t *testing.T) {
 	}
 }
 
-func TestBuildVSchemaPrimaryNonFunctionalFail(t *testing.T) {
-	bad := vschemapb.SrvVSchema{
-		Keyspaces: map[string]*vschemapb.Keyspace{
-			"sharded": {
-				Sharded: true,
-				Vindexes: map[string]*vschemapb.Vindex{
-					"stlu": {
-						Type:  "stlu",
-						Owner: "t1",
-					},
-				},
-				Tables: map[string]*vschemapb.Table{
-					"t1": {
-						ColumnVindexes: []*vschemapb.ColumnVindex{
-							{
-								Column: "c1",
-								Name:   "stlu",
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-	got, _ := BuildVSchema(&bad)
-	err := got.Keyspaces["sharded"].Error
-	want := "primary vindex stlu cannot be owned for table t1"
-	if err == nil || err.Error() != want {
-		t.Errorf("BuildVSchema: %v, want %v", err, want)
-	}
-}
-
 func TestSequence(t *testing.T) {
 	good := vschemapb.SrvVSchema{
 		Keyspaces: map[string]*vschemapb.Keyspace{
diff --git a/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go b/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go
index 5d46fb2eed..731de846b9 100644
--- a/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go
+++ b/go/vt/vttablet/tabletserver/vstreamer/planbuilder_test.go
@@ -362,7 +362,7 @@ func TestPlanbuilder(t *testing.T) {
 	}, {
 		inTable: t1,
 		inRule:  &binlogdatapb.Rule{Match: "t1", Filter: "select id, val from t1 where in_keyrange(id, 'lookup', '-80')"},
-		outErr:  `vindex must be Unique and Functional to be used for VReplication: lookup`,
+		outErr:  `vindex must be Unique to be used for VReplication: lookup`,
 	}, {
 		inTable: t1,
 		inRule:  &binlogdatapb.Rule{Match: "t1", Filter: "select id, val from t1 where in_keyrange(id, 'hash', '80')"},
diff --git a/test/vtgatev3_test.py b/test/vtgatev3_test.py
index 6d6e02743d..83039e40ef 100755
--- a/test/vtgatev3_test.py
+++ b/test/vtgatev3_test.py
@@ -1822,7 +1822,7 @@ class TestVTGateFunctions(unittest.TestCase):
     try:
       vtgate_conn.begin()
       with self.assertRaisesRegexp(dbexceptions.DatabaseError,
-                                   '.*could not map NULL to a keyspace id.*'):
+                                   '.*could not map.*NULL.*to a keyspace id.*'):
         self.execute_on_master(
             vtgate_conn,
             'insert into vt_user_extra (email) values (:email)',