Repository: ovn-org/libovsdb
Pull request #316 (2022-06-10 03:17:58)

client: add WhereAny() and let Where() take multiple models
There are cases where we'd like to take a list of models and return a list of results without grabbing the row cache lock for every single item. Batch them up into a Where() condition that can then be List()-ed right after, which is about 2x faster than Get()-ing each item individually.

Before:
```
BenchmarkAPIListMultiple/multiple_results_one_at_a_time_with_Get-8         	     212	   5534355 ns/op
BenchmarkAPIListMultiple/multiple_results_one_at_a_time_with_Get-8         	     212	   5640041 ns/op
BenchmarkAPIListMultiple/multiple_results_one_at_a_time_with_Get-8         	     206	   5680483 ns/op
```
After:
```
BenchmarkAPIListMultiple/multiple_results_in_a_batch_with_WhereAny-8       	     420	   2795212 ns/op
BenchmarkAPIListMultiple/multiple_results_in_a_batch_with_WhereAny-8       	     412	   2895229 ns/op
BenchmarkAPIListMultiple/multiple_results_in_a_batch_with_WhereAny-8       	     412	   2864806 ns/op
```

This PR also adds a WhereAny() call that acts like WhereAll() except with OR semantics, which is equivalent to the previous Where() call including conditions. Thus we now have:

1. Where()  - accepts one or more models and matches on indexes and model data
2. WhereAny() - takes a model and one or more conditions, matching each condition with logical OR
3. WhereAll()  - takes a model and one or more conditions, matching all conditions with a logical AND


@jcaamano @flavio-fernandes @trozet @tssurya

dcbw: > I have a feeling that this `WhereMany` is how `Where` should have been all along. What we could do is have `WhereAny` instead of `WhereMany`, that basically deprecates `Where`.

@jcaamano updated to be WhereAny(). I still need to add testcases.
coveralls: ## Pull Request Test Coverage Report for [Build 2539697443](https://coveralls.io/builds/50238067)

* **61** of **77**   **(79.22%)**  changed or added relevant lines in **4** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage decreased (**-0.06%**) to **74.687%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [client/client.go](https://coveralls.io/builds/50238067/source?filename=client%2Fclient.go#L1322) | 0 | 3 | 0.0%
| [client/api.go](https://coveralls.io/builds/50238067/source?filename=client%2Fapi.go#L183) | 10 | 14 | 71.43%
| [client/condition.go](https://coveralls.io/builds/50238067/source?filename=client%2Fcondition.go#L161) | 23 | 27 | 85.19%
| [cache/cache.go](https://coveralls.io/builds/50238067/source?filename=cache%2Fcache.go#L179) | 28 | 33 | 84.85%
<!-- | **Total:** | **61** | **77** | **79.22%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [client/api.go](https://coveralls.io/builds/50238067/source?filename=client%2Fapi.go#L185) | 1 | 80.61% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/50238067/badge)](https://coveralls.io/builds/50238067) |
| :-- | --: |
| Change from base [Build 2436166898](https://coveralls.io/builds/49721060): |  -0.06% |
| Covered Lines: | 4945 |
| Relevant Lines: | 6621 |

---
##### 💛  - [Coveralls](https://coveralls.io)

jcaamano: Wan't to run it by ovn-k to discard issues?

> @dcbw could you add tests where you pass models of different types, in the same places you added new tests? That should fail in some way, right?

Just cross-checking you read this. What I see is that you use the first model of the list to know which type it is and get the RowCache matching that type. And then in `RowsByModels` you skip any model that does not match that type. I am considering if an error would be more appropriate, as skipping can lead to the interpretation that the request made sense but the looked after models did not exist.


dcbw: > And then in `RowsByModels` you skip any model that does not match that type.

@jcaamano RowsByModels() doesn't return an error; I could make it return one though.

Patch:
diff --git a/README.md b/README.md
index 25339d9..7bcf7b3 100644
--- a/README.md
+++ b/README.md
@@ -49,11 +49,20 @@ Once the client object is created, a generic API can be used to interact with th
 
 Others, have to be called on a `ConditionalAPI` (`Update`, `Delete`, `Mutate`). There are three ways to create a `ConditionalAPI`:
 
-**Where()**: `Where()` can be used to create a `ConditionalAPI` using a list of Condition objects. Each condition object specifies a field using a pointer
+**Where()**: `Where()` can be used to create a `ConditionalAPI` based on the index information that the provided Models contain. Example:
+
+      ls := &LogicalSwitch{UUID: "foo"}
+      ls2 := &LogicalSwitch{UUID: "foo2"}
+      ops, _ := ovs.Where(ls, ls2).Delete()
+
+It will check the field corresponding to the `_uuid` column as well as all the other schema-defined or client-defined indexes in that order of priority.
+The first available index will be used to generate a condition.
+
+**WhereAny()**: `WhereAny()` can be used to create a `ConditionalAPI` using a list of Condition objects. Each condition object specifies a field using a pointer
 to a Model's field, a `ovsdb.ConditionFunction` and a value. The type of the value depends on the type of the field being mutated. Example:
 
       ls := &LogicalSwitch{}
-      ops, _ := ovs.Where(ls, client.Condition{
+      ops, _ := ovs.WhereAny(ls, client.Condition{
           Field: &ls.Config,
           Function: ovsdb.ConditionIncludes,
           Value: map[string]string{"foo": "bar"},
@@ -61,14 +70,9 @@ to a Model's field, a `ovsdb.ConditionFunction` and a value. The type of the val
 
 The resulting `ConditionalAPI` will create one operation per condition, so all the rows that match *any* of the specified conditions will be affected.
 
-If no conditions are provided, `Where()` will create a `ConditionalAPI` based on the index information that the provided Model contains.
-It will check the field corresponding to the `_uuid` column as well as all the other schema-defined or client-defined indexes in that order of priority.
-The first available index will be used to generate a condition.
-
-**WhereAll()**: `WhereAll()` behaves like `Where()` but with *AND* semantics. The resulting `ConditionalAPI` will put all the
+**WhereAll()**: `WhereAll()` behaves like `WhereAny()` but with *AND* semantics. The resulting `ConditionalAPI` will put all the
 conditions into a single operation. Therefore the operation will affect the rows that satisfy *all* the conditions.
 
-
 **WhereCache()**: `WhereCache()` uses a function callback to filter on the local cache. It's primary use is to perform cache operations such as
 `List()`. However, it can also be used to create server-side operations (such as `Delete()`, `Update()` or `Delete()`). If used this way, it will
 create an equality condition (using `ovsdb.ConditionEqual`) on the `_uuid` field for every matching row. Example:
diff --git a/cache/cache.go b/cache/cache.go
index c91fcf8..53ec71d 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -166,47 +166,60 @@ func (r *RowCache) Row(uuid string) model.Model {
 	return r.rowByUUID(uuid)
 }
 
-func (r *RowCache) rowsByModel(m model.Model, useClientIndexes bool) map[string]model.Model {
+// rowsByModels searches the cache to find all rows matching any of the provided
+// models, either by UUID or indexes. An error is returned if the model schema
+// has no UUID field, or if the provided models are not all the same type.
+func (r *RowCache) rowsByModels(models []model.Model, useClientIndexes bool) (map[string]model.Model, error) {
 	r.mutex.RLock()
 	defer r.mutex.RUnlock()
-	if reflect.TypeOf(m) != r.dataType {
-		return nil
-	}
-	info, _ := r.dbModel.NewModelInfo(m)
-	field, err := info.FieldByColumn("_uuid")
-	if err != nil {
-		return nil
-	}
-	uuid := field.(string)
-	if uuid != "" {
-		row := r.rowByUUID(uuid)
-		if row != nil {
-			return map[string]model.Model{uuid: row}
-		}
-	}
-	// indexSpecs are ordered, schema indexes go first, then client indexes
-	for _, indexSpec := range r.indexSpecs {
-		if indexSpec.isClientIndex() && !useClientIndexes {
-			// Given the ordered indexSpecs, we can break here if we reach the
-			// first client index
-			break
+
+	results := make(map[string]model.Model, len(models))
+	for _, m := range models {
+		if reflect.TypeOf(m) != r.dataType {
+			return nil, fmt.Errorf("model type %s didn't match expected row type %s", reflect.TypeOf(m), r.dataType)
 		}
-		val, err := valueFromIndex(info, indexSpec.columns)
+		info, _ := r.dbModel.NewModelInfo(m)
+		field, err := info.FieldByColumn("_uuid")
 		if err != nil {
-			continue
+			return nil, err
 		}
-		vals := r.indexes[indexSpec.index]
-		uuids, ok := vals[val]
-		if !ok || uuids.empty() {
-			continue
+		if uuid := field.(string); uuid != "" {
+			if _, ok := results[uuid]; !ok {
+				if row := r.rowByUUID(uuid); row != nil {
+					results[uuid] = row
+					continue
+				}
+			}
 		}
-		results := make(map[string]model.Model, len(uuids))
-		for uuid := range uuids {
-			results[uuid] = r.rowByUUID(uuid)
+
+		// indexSpecs are ordered, schema indexes go first, then client indexes
+		for _, indexSpec := range r.indexSpecs {
+			if indexSpec.isClientIndex() && !useClientIndexes {
+				// Given the ordered indexSpecs, we can break here if we reach the
+				// first client index
+				break
+			}
+			val, err := valueFromIndex(info, indexSpec.columns)
+			if err != nil {
+				continue
+			}
+			vals := r.indexes[indexSpec.index]
+			if uuids, ok := vals[val]; ok {
+				for uuid := range uuids {
+					if _, ok := results[uuid]; !ok {
+						results[uuid] = r.rowByUUID(uuid)
+					}
+				}
+				// Break after handling the first found index
+				// to ensure we preserve index order preference
+				break
+			}
 		}
-		return results
 	}
-	return nil
+	if len(results) == 0 {
+		return nil, nil
+	}
+	return results, nil
 }
 
 // RowByModel searches the cache by UUID and schema indexes. UUID search is
@@ -214,23 +227,26 @@ func (r *RowCache) rowsByModel(m model.Model, useClientIndexes bool) map[string]
 // with which they are defined in the schema. The model for the first matching
 // index is returned along with its UUID. An empty string and nil is returned if
 // no Model is found.
-func (r *RowCache) RowByModel(m model.Model) (string, model.Model) {
-	models := r.rowsByModel(m, false)
+func (r *RowCache) RowByModel(m model.Model) (string, model.Model, error) {
+	models, err := r.rowsByModels([]model.Model{m}, false)
+	if err != nil {
+		return "", nil, err
+	}
 	for uuid, model := range models {
-		return uuid, model
+		return uuid, model, nil
 	}
-	return "", nil
+	return "", nil, nil
 }
 
-// RowsByModel searches the cache by UUID, schema indexes and client indexes.
+// RowsByModels searches the cache by UUID, schema indexes and client indexes.
 // UUID search is performed first. Schema indexes are evaluated next in turn by
 // the same order with which they are defined in the schema. Finally, client
 // indexes are evaluated in turn by the same order with which they are defined
 // in the client DB model. The models for the first matching index are returned,
 // which might be more than 1 if they were found through a client index since in
 // that case uniqueness is not enforced. Nil is returned if no Model is found.
-func (r *RowCache) RowsByModel(m model.Model) map[string]model.Model {
-	return r.rowsByModel(m, true)
+func (r *RowCache) RowsByModels(models []model.Model) (map[string]model.Model, error) {
+	return r.rowsByModels(models, true)
 }
 
 // Create writes the provided content to the cache
diff --git a/client/api.go b/client/api.go
index 4d93eff..5665c51 100644
--- a/client/api.go
+++ b/client/api.go
@@ -26,14 +26,21 @@ type API interface {
 	// ConditionFromFunc(func(l *LogicalSwitch) bool { return l.Enabled })
 	WhereCache(predicate interface{}) ConditionalAPI
 
-	// Create a ConditionalAPI from a Model's index data or a list of Conditions
-	// where operations apply to elements that match any of the conditions
-	// If no condition is given, it will match the values provided in model.Model according
-	// to the indexes.
-	Where(model.Model, ...model.Condition) ConditionalAPI
-
-	// Create a ConditionalAPI from a Model's index data or a list of Conditions
-	// where operations apply to elements that match all the conditions
+	// Create a ConditionalAPI from a Model's index data, where operations
+	// apply to elements that match the values provided in one or more
+	// model.Models according to the indexes. All provided Models must be
+	// the same type or an error will be generated when operations are
+	// are performed on the ConditionalAPI.
+	Where(...model.Model) ConditionalAPI
+
+	// WhereAny creates a ConditionalAPI from a list of Conditions where
+	// operations apply to elements that match any (eg, logical OR) of the
+	// conditions.
+	WhereAny(model.Model, ...model.Condition) ConditionalAPI
+
+	// WhereAll creates a ConditionalAPI from a list of Conditions where
+	// operations apply to elements that match all (eg, logical AND) of the
+	// conditions.
 	WhereAll(model.Model, ...model.Condition) ConditionalAPI
 
 	// Get retrieves a model from the cache
@@ -168,17 +175,25 @@ func (a api) List(ctx context.Context, result interface{}) error {
 	return nil
 }
 
-// Where returns a conditionalAPI based on a Condition list
-func (a api) Where(model model.Model, cond ...model.Condition) ConditionalAPI {
-	return newConditionalAPI(a.cache, a.conditionFromModel(false, false, model, cond...), a.logger)
+// Where returns a conditionalAPI based on model indexes. All provided models
+// must be the same type.
+func (a api) Where(models ...model.Model) ConditionalAPI {
+	return newConditionalAPI(a.cache, a.conditionFromModels(models), a.logger)
 }
 
-// Where returns a conditionalAPI based on a Condition list
-func (a api) WhereAll(model model.Model, cond ...model.Condition) ConditionalAPI {
-	return newConditionalAPI(a.cache, a.conditionFromModel(true, false, model, cond...), a.logger)
+// WhereAny returns a conditionalAPI based on a Condition list that matches any
+// of the conditions individually
+func (a api) WhereAny(m model.Model, cond ...model.Condition) ConditionalAPI {
+	return newConditionalAPI(a.cache, a.conditionFromExplicitConditions(false, m, cond...), a.logger)
 }
 
-// Where returns a conditionalAPI based a Predicate
+// WhereAll returns a conditionalAPI based on a Condition list that matches all
+// of the conditions together
+func (a api) WhereAll(m model.Model, cond ...model.Condition) ConditionalAPI {
+	return newConditionalAPI(a.cache, a.conditionFromExplicitConditions(true, m, cond...), a.logger)
+}
+
+// WhereCache returns a conditionalAPI based a Predicate
 func (a api) WhereCache(predicate interface{}) ConditionalAPI {
 	return newConditionalAPI(a.cache, a.conditionFromFunc(predicate), a.logger)
 }
@@ -198,26 +213,37 @@ func (a api) conditionFromFunc(predicate interface{}) Conditional {
 	return condition
 }
 
-// FromModel returns a Condition from a model and a list of fields
-func (a api) conditionFromModel(any bool, cache bool, model model.Model, cond ...model.Condition) Conditional {
-	var conditional Conditional
-	var err error
-
-	tableName, err := a.getTableFromModel(model)
+// conditionFromModels returns a Conditional from one or more models.
+func (a api) conditionFromModels(models []model.Model) Conditional {
+	if len(models) == 0 {
+		return newErrorConditional(fmt.Errorf("at least one model required"))
+	}
+	tableName, err := a.getTableFromModel(models[0])
 	if tableName == "" {
 		return newErrorConditional(err)
 	}
+	conditional, err := newEqualityConditional(tableName, a.cache, models)
+	if err != nil {
+		return newErrorConditional(err)
+	}
+	return conditional
+}
 
+// conditionFromExplicitConditions returns a Conditional from a model and a set
+// of explicit conditions. If matchAll is true, then models that match all the given
+// conditions are selected by the Conditional. If matchAll is false, then any model
+// that matches one of the conditions is selected.
+func (a api) conditionFromExplicitConditions(matchAll bool, m model.Model, cond ...model.Condition) Conditional {
 	if len(cond) == 0 {
-		conditional, err = newEqualityConditional(tableName, a.cache, model)
-		if err != nil {
-			conditional = newErrorConditional(err)
-		}
-	} else {
-		conditional, err = newExplicitConditional(tableName, a.cache, any, model, cond...)
-		if err != nil {
-			conditional = newErrorConditional(err)
-		}
+		return newErrorConditional(fmt.Errorf("at least one condition is required"))
+	}
+	tableName, err := a.getTableFromModel(m)
+	if tableName == "" {
+		return newErrorConditional(err)
+	}
+	conditional, err := newExplicitConditional(tableName, a.cache, matchAll, m, cond...)
+	if err != nil {
+		return newErrorConditional(err)
 	}
 	return conditional
 }
@@ -239,8 +265,10 @@ func (a api) Get(ctx context.Context, m model.Model) error {
 		return ErrNotFound
 	}
 
-	_, found := tableCache.RowByModel(m)
-	if found == nil {
+	_, found, err := tableCache.RowByModel(m)
+	if err != nil {
+		return err
+	} else if found == nil {
 		return ErrNotFound
 	}
 
diff --git a/client/client.go b/client/client.go
index 6ae93ee..ffd36a0 100644
--- a/client/client.go
+++ b/client/client.go
@@ -178,7 +178,7 @@ func newOVSDBClient(clientDBModel model.ClientDBModel, opts ...Option) (*ovsdbCl
 		)
 		ovs.logger = &l
 	}
-	ovs.metrics.init(clientDBModel.Name(), ovs.options.metricNamespace, ovs.options.metricSubsystem)
+	ovs.metrics.init(clientDBModel.Name())
 	ovs.registerMetrics()
 
 	// if we should only connect to the leader, then add the special "_Server" database as well
@@ -1312,9 +1312,14 @@ func (o *ovsdbClient) List(ctx context.Context, result interface{}) error {
 	return primaryDB.api.List(ctx, result)
 }
 
-//Where implements the API interface's Where function
-func (o *ovsdbClient) Where(m model.Model, conditions ...model.Condition) ConditionalAPI {
-	return o.primaryDB().api.Where(m, conditions...)
+// Where implements the API interface's Where function
+func (o *ovsdbClient) Where(models ...model.Model) ConditionalAPI {
+	return o.primaryDB().api.Where(models...)
+}
+
+// WhereAny implements the API interface's WhereAny function
+func (o *ovsdbClient) WhereAny(m model.Model, conditions ...model.Condition) ConditionalAPI {
+	return o.primaryDB().api.WhereAny(m, conditions...)
 }
 
 //WhereAll implements the API interface's WhereAll function
diff --git a/client/condition.go b/client/condition.go
index 759469a..1dfabda 100644
--- a/client/condition.go
+++ b/client/condition.go
@@ -62,7 +62,7 @@ func generateOvsdbConditionsFromModelConditions(dbModel model.DatabaseModel, inf
 // matching model in the database.
 type equalityConditional struct {
 	tableName string
-	model     model.Model
+	models    []model.Model
 	cache     *cache.TableCache
 }
 
@@ -77,7 +77,7 @@ func (c *equalityConditional) Matches() (map[string]model.Model, error) {
 	if tableCache == nil {
 		return nil, ErrNotFound
 	}
-	return tableCache.RowsByModel(c.model), nil
+	return tableCache.RowsByModels(c.models)
 }
 
 // Generate conditions based on the equality of the first available index. If
@@ -85,19 +85,28 @@ func (c *equalityConditional) Matches() (map[string]model.Model, error) {
 // based on the UUID of the found model. Otherwise, the conditions will be based
 // on the index.
 func (c *equalityConditional) Generate() ([][]ovsdb.Condition, error) {
-	models, _ := c.Matches()
+	models, err := c.Matches()
+	if err != nil && err != ErrNotFound {
+		return nil, err
+	}
 	if len(models) == 0 {
-		// no cache hits, generate condition from model we were given
-		return generateConditionsFromModels(c.cache.DatabaseModel(), map[string]model.Model{"": c.model})
+		// no cache hits, generate condition from models we were given
+		modelMap := make(map[string]model.Model, len(c.models))
+		for i, m := range c.models {
+			// generateConditionsFromModels() ignores the map keys
+			// so just use the range index
+			modelMap[fmt.Sprintf("%d", i)] = m
+		}
+		return generateConditionsFromModels(c.cache.DatabaseModel(), modelMap)
 	}
 	return generateConditionsFromModels(c.cache.DatabaseModel(), models)
 }
 
 // NewEqualityCondition creates a new equalityConditional
-func newEqualityConditional(table string, cache *cache.TableCache, model model.Model) (Conditional, error) {
+func newEqualityConditional(table string, cache *cache.TableCache, models []model.Model) (Conditional, error) {
 	return &equalityConditional{
 		tableName: table,
-		model:     model,
+		models:    models,
 		cache:     cache,
 	}, nil
 }
@@ -134,7 +143,10 @@ func (c *explicitConditional) Matches() (map[string]model.Model, error) {
 
 // Generate returns conditions based on the provided Condition list
 func (c *explicitConditional) Generate() ([][]ovsdb.Condition, error) {
-	models, _ := c.Matches()
+	models, err := c.Matches()
+	if err != nil && err != ErrNotFound {
+		return nil, err
+	}
 	if len(models) == 0 {
 		// no cache hits, return conditions we were given
 		return c.anyConditions, nil
@@ -143,13 +155,13 @@ func (c *explicitConditional) Generate() ([][]ovsdb.Condition, error) {
 }
 
 // newExplicitConditional creates a new explicitConditional
-func newExplicitConditional(table string, cache *cache.TableCache, all bool, model model.Model, cond ...model.Condition) (Conditional, error) {
+func newExplicitConditional(table string, cache *cache.TableCache, matchAll bool, model model.Model, cond ...model.Condition) (Conditional, error) {
 	dbModel := cache.DatabaseModel()
 	info, err := dbModel.NewModelInfo(model)
 	if err != nil {
 		return nil, err
 	}
-	anyConditions, err := generateOvsdbConditionsFromModelConditions(dbModel, info, cond, all)
+	anyConditions, err := generateOvsdbConditionsFromModelConditions(dbModel, info, cond, matchAll)
 	if err != nil {
 		return nil, err
 	}
@@ -195,7 +207,10 @@ func (c *predicateConditional) Table() string {
 // generate returns a list of conditions that match, by _uuid equality, all the objects that
 // match the predicate
 func (c *predicateConditional) Generate() ([][]ovsdb.Condition, error) {
-	models, _ := c.Matches()
+	models, err := c.Matches()
+	if err != nil {
+		return nil, err
+	}
 	return generateConditionsFromModels(c.cache.DatabaseModel(), models)
 }
 
diff --git a/client/metrics.go b/client/metrics.go
index a8970df..72af036 100644
--- a/client/metrics.go
+++ b/client/metrics.go
@@ -1,12 +1,8 @@
 package client
 
-import (
-	"sync"
+import "github.com/prometheus/client_golang/prometheus"
 
-	"github.com/prometheus/client_golang/prometheus"
-)
-
-const libovsdbName = "libovsdb"
+const namespace = "libovsdb"
 
 type metrics struct {
 	numUpdates      *prometheus.CounterVec
@@ -15,23 +11,15 @@ type metrics struct {
 	numMonitors     prometheus.Gauge
 }
 
-var regMetricsOnce sync.Once
-
-func (m *metrics) init(modelName string, namespace, subsystem string) {
+func (m *metrics) init(modelName string) {
 	// labels that are the same across all metrics
 	constLabels := prometheus.Labels{"primary_model": modelName}
 
-	if namespace == "" {
-		namespace = libovsdbName
-		subsystem = ""
-	}
-
 	m.numUpdates = prometheus.NewCounterVec(
 		prometheus.CounterOpts{
 			Namespace:   namespace,
-			Subsystem:   subsystem,
 			Name:        "update_messages_total",
-			Help:        "Count of libovsdb monitor update messages processed, partitioned by database",
+			Help:        "Count of monitor update messages processed, partitioned by database",
 			ConstLabels: constLabels,
 		},
 		[]string{"database"},
@@ -40,9 +28,8 @@ func (m *metrics) init(modelName string, namespace, subsystem string) {
 	m.numTableUpdates = prometheus.NewCounterVec(
 		prometheus.CounterOpts{
 			Namespace:   namespace,
-			Subsystem:   subsystem,
 			Name:        "table_updates_total",
-			Help:        "Count of libovsdb monitor update messages per table",
+			Help:        "Count of monitor update messages per table",
 			ConstLabels: constLabels,
 		},
 		[]string{"database", "table"},
@@ -51,9 +38,8 @@ func (m *metrics) init(modelName string, namespace, subsystem string) {
 	m.numDisconnects = prometheus.NewCounter(
 		prometheus.CounterOpts{
 			Namespace:   namespace,
-			Subsystem:   subsystem,
 			Name:        "disconnects_total",
-			Help:        "Count of libovsdb disconnects encountered",
+			Help:        "Count of disconnects encountered",
 			ConstLabels: constLabels,
 		},
 	)
@@ -61,23 +47,21 @@ func (m *metrics) init(modelName string, namespace, subsystem string) {
 	m.numMonitors = prometheus.NewGauge(
 		prometheus.GaugeOpts{
 			Namespace:   namespace,
-			Subsystem:   subsystem,
 			Name:        "monitors",
-			Help:        "Number of running libovsdb ovsdb monitors",
+			Help:        "Number of running ovsdb monitors",
 			ConstLabels: constLabels,
 		},
 	)
+
 }
 
 func (m *metrics) register(r prometheus.Registerer) {
-	regMetricsOnce.Do(func() {
-		r.MustRegister(
-			m.numUpdates,
-			m.numTableUpdates,
-			m.numDisconnects,
-			m.numMonitors,
-		)
-	})
+	r.MustRegister(
+		m.numUpdates,
+		m.numTableUpdates,
+		m.numDisconnects,
+		m.numMonitors,
+	)
 }
 
 func (o *ovsdbClient) registerMetrics() {
diff --git a/client/options.go b/client/options.go
index 5ebf292..71e2524 100644
--- a/client/options.go
+++ b/client/options.go
@@ -25,9 +25,7 @@ type options struct {
 	backoff               backoff.BackOff
 	logger                *logr.Logger
 	registry              prometheus.Registerer
-	shouldRegisterMetrics bool   // in case metrics are changed after-the-fact
-	metricNamespace       string // prometheus metric namespace
-	metricSubsystem       string // prometheus metric subsystem
+	shouldRegisterMetrics bool // in case metrics are changed after-the-fact
 }
 
 type Option func(o *options) error
@@ -129,19 +127,3 @@ func WithMetricsRegistry(r prometheus.Registerer) Option {
 		return nil
 	}
 }
-
-// WithMetricsRegistryNamespaceSubsystem allows the user to specify a Prometheus metrics registry
-// and Prometheus metric namespace and subsystem of the component utilizing libovsdb.
-// If supplied, the metrics as defined in metrics.go will be registered.
-func WithMetricsRegistryNamespaceSubsystem(r prometheus.Registerer, namespace, subsystem string) Option {
-	if namespace == "" || subsystem == "" {
-		panic("libovsdb function WithMetricsRegistryNamespaceSubsystem arguments 'namespace' and 'subsystem' must not be empty")
-	}
-	return func(o *options) error {
-		o.registry = r
-		o.shouldRegisterMetrics = (r != nil)
-		o.metricNamespace = namespace
-		o.metricSubsystem = subsystem
-		return nil
-	}
-}


Test patch:
diff --git a/cache/cache_test.go b/cache/cache_test.go
index b4e7a07..303b1cf 100644
--- a/cache/cache_test.go
+++ b/cache/cache_test.go
@@ -1659,7 +1659,8 @@ func TestTableCachePopulate2BrokenIndexes(t *testing.T) {
 	assert.False(t, ok)
 
 	t.Log("Lookup Original Insert By Index")
-	_, result := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "bar"})
+	_, result, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "bar"})
+	require.NoError(t, err)
 	require.NotNil(t, result)
 }
 
@@ -1841,20 +1842,32 @@ func TestTableCacheRowByModelSingleIndex(t *testing.T) {
 	myFoo, tc := setupRowByModelSingleIndex(t)
 
 	t.Run("get foo by index", func(t *testing.T) {
-		_, foo := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo"})
+		_, foo, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo"})
+		assert.NoError(t, err)
 		assert.NotNil(t, foo)
 		assert.Equal(t, myFoo, foo)
 	})
 
 	t.Run("get non-existent item by index", func(t *testing.T) {
-		_, baz := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "baz"})
+		_, baz, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "baz"})
+		assert.NoError(t, err)
 		assert.Nil(t, baz)
 	})
 
 	t.Run("no index data", func(t *testing.T) {
-		_, foo := tc.Table("Open_vSwitch").RowByModel(&testModel{Bar: "foo"})
+		_, foo, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Bar: "foo"})
+		assert.NoError(t, err)
 		assert.Nil(t, foo)
 	})
+
+	t.Run("wrong model type", func(t *testing.T) {
+		type badModel struct {
+			UUID string `ovsdb:"_uuid"`
+			Baz  string `ovsdb:"baz"`
+		}
+		_, _, err := tc.Table("Open_vSwitch").RowByModel(&badModel{Baz: "baz"})
+		assert.Error(t, err)
+	})
 }
 
 func benchmarkDoCreate(b *testing.B, numRows int) (*TableCache, *RowCache) {
@@ -2040,19 +2053,22 @@ func TestTableCacheRowByModelTwoIndexes(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("get foo by Foo index", func(t *testing.T) {
-		_, foo := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo"})
+		_, foo, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo"})
+		assert.NoError(t, err)
 		assert.NotNil(t, foo)
 		assert.Equal(t, myFoo, foo)
 	})
 
 	t.Run("get foo by Bar index", func(t *testing.T) {
-		_, foo := tc.Table("Open_vSwitch").RowByModel(&testModel{Bar: "foo"})
+		_, foo, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Bar: "foo"})
+		assert.NoError(t, err)
 		assert.NotNil(t, foo)
 		assert.Equal(t, myFoo, foo)
 	})
 
 	t.Run("get non-existent item by index", func(t *testing.T) {
-		_, baz := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "baz"})
+		_, baz, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "baz"})
+		assert.NoError(t, err)
 		assert.Nil(t, baz)
 	})
 
@@ -2074,18 +2090,21 @@ func TestTableCacheRowByModelMultiIndex(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("incomplete index", func(t *testing.T) {
-		_, foo := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo"})
+		_, foo, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo"})
+		assert.NoError(t, err)
 		assert.Nil(t, foo)
 	})
 
 	t.Run("get foo by index", func(t *testing.T) {
-		_, foo := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo", Bar: "foo"})
+		_, foo, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "foo", Bar: "foo"})
+		assert.NoError(t, err)
 		assert.NotNil(t, foo)
 		assert.Equal(t, myFoo, foo)
 	})
 
 	t.Run("get non-existent item by index", func(t *testing.T) {
-		_, baz := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "baz", Bar: "baz"})
+		_, baz, err := tc.Table("Open_vSwitch").RowByModel(&testModel{Foo: "baz", Bar: "baz"})
+		assert.NoError(t, err)
 		assert.Nil(t, baz)
 	})
 }
@@ -2290,7 +2309,7 @@ func TestTableCacheApplyModifications(t *testing.T) {
 	}
 }
 
-func TestTableCacheRowsByModel(t *testing.T) {
+func TestTableCacheRowsByModels(t *testing.T) {
 	type testModel struct {
 		UUID string            `ovsdb:"_uuid"`
 		Foo  string            `ovsdb:"foo"`
@@ -2362,49 +2381,85 @@ func TestTableCacheRowsByModel(t *testing.T) {
 	require.Empty(t, errs)
 
 	tests := []struct {
-		name  string
-		model model.Model
-		rows  map[string]model.Model
+		name   string
+		models []model.Model
+		rows   map[string]model.Model
 	}{
 		{
-			"by non index, no result",
-			&testModel{Foo: "no", Bar: "no", Baz: map[string]string{"baz": "no"}},
-			nil,
+			name: "by non index, no result",
+			models: []model.Model{
+				&testModel{Foo: "no", Bar: "no", Baz: map[string]string{"baz": "no"}},
+			},
+			rows: nil,
 		},
 		{
-			"by single column client index, single result",
-			&testModel{Bar: "foo"},
-			map[string]model.Model{
+			name: "by single column client index, single result",
+			models: []model.Model{
+				&testModel{Bar: "foo"},
+			},
+			rows: map[string]model.Model{
 				"foo": testData["Open_vSwitch"]["foo"],
 			},
 		},
 		{
-			"by single column client index, multiple results",
-			&testModel{Bar: "bar"},
-			map[string]model.Model{
+			name: "by single column client index, multiple models, multiple results",
+			models: []model.Model{
+				&testModel{Bar: "foo"},
+				&testModel{Bar: "baz"},
+			},
+			rows: map[string]model.Model{
+				"foo": testData["Open_vSwitch"]["foo"],
+				"baz": testData["Open_vSwitch"]["baz"],
+			},
+		},
+		{
+			name: "by single column client index, multiple results",
+			models: []model.Model{
+				&testModel{Bar: "bar"},
+			},
+			rows: map[string]model.Model{
 				"bar":    testData["Open_vSwitch"]["bar"],
 				"foobar": testData["Open_vSwitch"]["foobar"],
 			},
 		},
 		{
-			"by multi column client index, single result",
-			&testModel{Bar: "baz", Baz: map[string]string{"baz": "baz"}},
-			map[string]model.Model{
+			name: "by multi column client index, single result",
+			models: []model.Model{
+				&testModel{Bar: "baz", Baz: map[string]string{"baz": "baz"}},
+			},
+			rows: map[string]model.Model{
 				"baz": testData["Open_vSwitch"]["baz"],
 			},
 		},
 		{
-			"by client index, multiple results",
-			&testModel{Bar: "quux", Baz: map[string]string{"baz": "quux"}},
-			map[string]model.Model{
+			name: "by client index, multiple results",
+			models: []model.Model{
+				&testModel{Bar: "quux", Baz: map[string]string{"baz": "quux"}},
+			},
+			rows: map[string]model.Model{
 				"quux": testData["Open_vSwitch"]["quux"],
 				"quuz": testData["Open_vSwitch"]["quuz"],
 			},
 		},
 		{
-			"by schema index prioritized over client index",
-			&testModel{Foo: "foo", Bar: "bar", Baz: map[string]string{"baz": "bar"}},
-			map[string]model.Model{
+			name: "by client index, multiple models, multiple results",
+			models: []model.Model{
+				&testModel{Bar: "quux", Baz: map[string]string{"baz": "quux"}},
+				&testModel{Bar: "bar", Baz: map[string]string{"baz": "foobar"}},
+			},
+			rows: map[string]model.Model{
+				"quux":   testData["Open_vSwitch"]["quux"],
+				"quuz":   testData["Open_vSwitch"]["quuz"],
+				"foobar": testData["Open_vSwitch"]["foobar"],
+				"bar":    testData["Open_vSwitch"]["bar"],
+			},
+		},
+		{
+			name: "by schema index prioritized over client index",
+			models: []model.Model{
+				&testModel{Foo: "foo", Bar: "bar", Baz: map[string]string{"baz": "bar"}},
+			},
+			rows: map[string]model.Model{
 				"foo": testData["Open_vSwitch"]["foo"],
 			},
 		},
@@ -2413,7 +2468,8 @@ func TestTableCacheRowsByModel(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			tc, err := NewTableCache(dbModel, testData, nil)
 			require.NoError(t, err)
-			rows := tc.Table("Open_vSwitch").RowsByModel(tt.model)
+			rows, err := tc.Table("Open_vSwitch").RowsByModels(tt.models)
+			require.NoError(t, err)
 			require.Equal(t, tt.rows, rows)
 		})
 	}
diff --git a/client/api_test.go b/client/api_test.go
index f731f10..7c76810 100644
--- a/client/api_test.go
+++ b/client/api_test.go
@@ -173,6 +173,13 @@ func TestAPIListSimple(t *testing.T) {
 		assert.Nil(t, err)
 		assert.Len(t, result, len(lscacheList))
 	})
+
+	t.Run("ApiList: fails if conditional is an error", func(t *testing.T) {
+		result := []testLogicalSwitch{}
+		api := newConditionalAPI(tcache, newErrorConditional(fmt.Errorf("error")), &discardLogger)
+		err := api.List(context.Background(), &result)
+		assert.NotNil(t, err)
+	})
 }
 
 func TestAPIListPredicate(t *testing.T) {
@@ -343,7 +350,7 @@ func TestAPIListWhereConditions(t *testing.T) {
 			if tt.matchAll {
 				capi = api.WhereAll(testObj, conds...)
 			} else {
-				capi = api.Where(testObj, conds...)
+				capi = api.WhereAny(testObj, conds...)
 			}
 			err := capi.List(context.Background(), &result)
 			assert.NoError(t, err)
@@ -440,6 +447,92 @@ func TestAPIListFields(t *testing.T) {
 	})
 }
 
+func TestAPIListMulti(t *testing.T) {
+	lspcacheList := []model.Model{
+		&testLogicalSwitchPort{
+			UUID:        aUUID0,
+			Name:        "lsp0",
+			ExternalIds: map[string]string{"foo": "bar"},
+			Enabled:     &trueVal,
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID1,
+			Name:        "magiclsp1",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     &falseVal,
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID2,
+			Name:        "lsp2",
+			ExternalIds: map[string]string{"unique": "id"},
+			Enabled:     &falseVal,
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID3,
+			Name:        "magiclsp2",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     &trueVal,
+		},
+	}
+	lspcache := map[string]model.Model{}
+	for i := range lspcacheList {
+		lspcache[lspcacheList[i].(*testLogicalSwitchPort).UUID] = lspcacheList[i]
+	}
+	testData := cache.Data{
+		"Logical_Switch_Port": lspcache,
+	}
+	tcache := apiTestCache(t, testData)
+
+	test := []struct {
+		name    string
+		models  []model.Model
+		matches []model.Model
+		err     bool
+	}{
+		{
+			name: "No match",
+			models: []model.Model{
+				&testLogicalSwitchPort{UUID: "asdfasdfaf"},
+				&testLogicalSwitchPort{UUID: "ghghghghgh"},
+			},
+			matches: []model.Model{},
+			err:     false,
+		},
+		{
+			name: "One match",
+			models: []model.Model{
+				&testLogicalSwitchPort{UUID: aUUID0},
+				&testLogicalSwitchPort{UUID: "ghghghghgh"},
+			},
+			matches: []model.Model{lspcache[aUUID0]},
+			err:     false,
+		},
+		{
+			name: "Mismatched models",
+			models: []model.Model{
+				&testLogicalSwitchPort{UUID: aUUID0},
+				&testLogicalSwitch{UUID: "ghghghghgh"},
+			},
+			matches: []model.Model{},
+			err:     true,
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			var result []*testLogicalSwitchPort
+			api := newAPI(tcache, &discardLogger)
+			err := api.Where(tt.models...).List(context.Background(), &result)
+			if tt.err {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.ElementsMatchf(t, tt.matches, result, "Content should match")
+			}
+		})
+	}
+}
+
 func TestConditionFromFunc(t *testing.T) {
 	test := []struct {
 		name string
@@ -486,32 +579,46 @@ func TestConditionFromFunc(t *testing.T) {
 func TestConditionFromModel(t *testing.T) {
 	var testObj testLogicalSwitch
 	test := []struct {
-		name  string
-		model model.Model
-		conds []model.Condition
-		err   bool
+		name   string
+		models []model.Model
+		conds  []model.Condition
+		err    bool
 	}{
 		{
-			name:  "wrong model must fail",
-			model: &struct{ a string }{},
-			err:   true,
+			name: "wrong model must fail",
+			models: []model.Model{
+				&struct{ a string }{},
+			},
+			err: true,
 		},
 		{
 			name: "wrong condition must fail",
-			model: &struct {
-				a string `ovsdb:"_uuid"`
-			}{},
+			models: []model.Model{
+				&struct {
+					a string `ovsdb:"_uuid"`
+				}{},
+			},
 			conds: []model.Condition{{Field: "foo"}},
 			err:   true,
 		},
 		{
-			name:  "correct model must succeed",
-			model: &testLogicalSwitch{},
-			err:   false,
+			name: "correct model must succeed",
+			models: []model.Model{
+				&testLogicalSwitch{},
+			},
+			err: false,
+		},
+		{
+			name: "correct models must succeed",
+			models: []model.Model{
+				&testLogicalSwitch{},
+				&testLogicalSwitch{},
+			},
+			err: false,
 		},
 		{
-			name:  "correct model with valid condition must succeed",
-			model: &testObj,
+			name:   "correct model with valid condition must succeed",
+			models: []model.Model{&testObj},
 			conds: []model.Condition{
 				{
 					Field:    &testObj.Name,
@@ -529,10 +636,15 @@ func TestConditionFromModel(t *testing.T) {
 	}
 
 	for _, tt := range test {
-		t.Run(fmt.Sprintf("conditionFromModel: %s", tt.name), func(t *testing.T) {
+		t.Run(fmt.Sprintf("conditionFromModels: %s", tt.name), func(t *testing.T) {
 			cache := apiTestCache(t, nil)
 			apiIface := newAPI(cache, &discardLogger)
-			condition := apiIface.(api).conditionFromModel(false, false, tt.model, tt.conds...)
+			var condition Conditional
+			if len(tt.conds) > 0 {
+				condition = apiIface.(api).conditionFromExplicitConditions(true, tt.models[0], tt.conds...)
+			} else {
+				condition = apiIface.(api).conditionFromModels(tt.models)
+			}
 			if tt.err {
 				assert.IsType(t, &errorConditional{}, condition)
 			} else {
@@ -808,6 +920,44 @@ func TestAPIMutate(t *testing.T) {
 			},
 			err: false,
 		},
+		{
+			name: "select multiple by UUID addElement to set",
+			condition: func(a API) ConditionalAPI {
+				return a.Where(
+					&testLogicalSwitchPort{UUID: aUUID0},
+					&testLogicalSwitchPort{UUID: aUUID1},
+					&testLogicalSwitchPort{UUID: aUUID2},
+				)
+			},
+			mutations: []model.Mutation{
+				{
+					Field:   &testObj.Addresses,
+					Mutator: ovsdb.MutateOperationInsert,
+					Value:   []string{"2.2.2.2"},
+				},
+			},
+			result: []ovsdb.Operation{
+				{
+					Op:        ovsdb.OperationMutate,
+					Table:     "Logical_Switch_Port",
+					Mutations: []ovsdb.Mutation{{Column: "addresses", Mutator: ovsdb.MutateOperationInsert, Value: testOvsSet(t, []string{"2.2.2.2"})}},
+					Where:     []ovsdb.Condition{{Column: "_uuid", Function: ovsdb.ConditionEqual, Value: ovsdb.UUID{GoUUID: aUUID0}}},
+				},
+				{
+					Op:        ovsdb.OperationMutate,
+					Table:     "Logical_Switch_Port",
+					Mutations: []ovsdb.Mutation{{Column: "addresses", Mutator: ovsdb.MutateOperationInsert, Value: testOvsSet(t, []string{"2.2.2.2"})}},
+					Where:     []ovsdb.Condition{{Column: "_uuid", Function: ovsdb.ConditionEqual, Value: ovsdb.UUID{GoUUID: aUUID1}}},
+				},
+				{
+					Op:        ovsdb.OperationMutate,
+					Table:     "Logical_Switch_Port",
+					Mutations: []ovsdb.Mutation{{Column: "addresses", Mutator: ovsdb.MutateOperationInsert, Value: testOvsSet(t, []string{"2.2.2.2"})}},
+					Where:     []ovsdb.Condition{{Column: "_uuid", Function: ovsdb.ConditionEqual, Value: ovsdb.UUID{GoUUID: aUUID2}}},
+				},
+			},
+			err: false,
+		},
 		{
 			name: "select by name delete element from map with cache",
 			condition: func(a API) ConditionalAPI {
@@ -920,6 +1070,24 @@ func TestAPIMutate(t *testing.T) {
 			mutations: []model.Mutation{},
 			err:       true,
 		},
+		{
+			name: "multiple different selected models must fail",
+			condition: func(a API) ConditionalAPI {
+				return a.Where(
+					&testLogicalSwitchPort{UUID: aUUID0},
+					&testLogicalSwitchPort{UUID: aUUID1},
+					&testLogicalSwitch{UUID: aUUID2},
+				)
+			},
+			err: true,
+		},
+		{
+			name: "fails if conditional is an error",
+			condition: func(a API) ConditionalAPI {
+				return newConditionalAPI(nil, newErrorConditional(fmt.Errorf("error")), &discardLogger)
+			},
+			err: true,
+		},
 	}
 	for _, tt := range test {
 		t.Run(fmt.Sprintf("ApiMutate: %s", tt.name), func(t *testing.T) {
@@ -929,7 +1097,7 @@ func TestAPIMutate(t *testing.T) {
 			if tt.err {
 				require.Error(t, err)
 			} else {
-				require.Nil(t, err)
+				require.NoError(t, err)
 				assert.ElementsMatchf(t, tt.result, ops, "ovsdb.Operations should match")
 			}
 		})
@@ -1096,7 +1264,7 @@ func TestAPIUpdate(t *testing.T) {
 					Type:    "sometype",
 					Enabled: &trueVal,
 				}
-				return a.Where(&t, model.Condition{
+				return a.WhereAny(&t, model.Condition{
 					Field:    &t.Type,
 					Function: ovsdb.ConditionEqual,
 					Value:    "sometype",
@@ -1116,18 +1284,18 @@ func TestAPIUpdate(t *testing.T) {
 			err: false,
 		},
 		{
-			name: "multiple select any by field change multiple field",
+			name: "multiple select any by field change multiple field with cache hits",
 			condition: func(a API) ConditionalAPI {
 				t := testLogicalSwitchPort{}
-				return a.Where(&t,
+				return a.WhereAny(&t,
 					model.Condition{
 						Field:    &t.Type,
 						Function: ovsdb.ConditionEqual,
-						Value:    "sometype",
+						Value:    "someOtherType",
 					},
 					model.Condition{
 						Field:    &t.Enabled,
-						Function: ovsdb.ConditionIncludes,
+						Function: ovsdb.ConditionEqual,
 						Value:    &trueVal,
 					})
 			},
@@ -1139,13 +1307,19 @@ func TestAPIUpdate(t *testing.T) {
 					Op:    ovsdb.OperationUpdate,
 					Table: "Logical_Switch_Port",
 					Row:   tagRow,
-					Where: []ovsdb.Condition{{Column: "type", Function: ovsdb.ConditionEqual, Value: "sometype"}},
+					Where: []ovsdb.Condition{{Column: "_uuid", Function: ovsdb.ConditionEqual, Value: ovsdb.UUID{GoUUID: aUUID2}}},
 				},
 				{
 					Op:    ovsdb.OperationUpdate,
 					Table: "Logical_Switch_Port",
 					Row:   tagRow,
-					Where: []ovsdb.Condition{{Column: "enabled", Function: ovsdb.ConditionIncludes, Value: testOvsSet(t, &trueVal)}},
+					Where: []ovsdb.Condition{{Column: "_uuid", Function: ovsdb.ConditionEqual, Value: ovsdb.UUID{GoUUID: aUUID0}}},
+				},
+				{
+					Op:    ovsdb.OperationUpdate,
+					Table: "Logical_Switch_Port",
+					Row:   tagRow,
+					Where: []ovsdb.Condition{{Column: "_uuid", Function: ovsdb.ConditionEqual, Value: ovsdb.UUID{GoUUID: aUUID1}}},
 				},
 			},
 			err: false,
@@ -1189,7 +1363,7 @@ func TestAPIUpdate(t *testing.T) {
 					Type:    "someType",
 					Enabled: &trueVal,
 				}
-				return a.Where(&t, model.Condition{
+				return a.WhereAny(&t, model.Condition{
 					Field:    &t.Type,
 					Function: ovsdb.ConditionNotEqual,
 					Value:    "someType",
@@ -1215,7 +1389,7 @@ func TestAPIUpdate(t *testing.T) {
 					Type:    "sometype",
 					Enabled: &trueVal,
 				}
-				return a.Where(&t, model.Condition{
+				return a.WhereAny(&t, model.Condition{
 					Field:    &t.Tag,
 					Function: ovsdb.ConditionNotEqual,
 					Value:    &one,
@@ -1261,6 +1435,26 @@ func TestAPIUpdate(t *testing.T) {
 			},
 			err: false,
 		},
+		{
+			name: "multiple different selected models must fail",
+			condition: func(a API) ConditionalAPI {
+				return a.Where(
+					&testLogicalSwitchPort{UUID: aUUID0},
+					&testLogicalSwitchPort{UUID: aUUID1},
+					&testLogicalSwitch{UUID: aUUID2},
+				)
+			},
+			err: true,
+		},
+		{
+			name: "fails if conditional is an error",
+			condition: func(a API) ConditionalAPI {
+				return newConditionalAPI(tcache, newErrorConditional(fmt.Errorf("error")), &discardLogger)
+			},
+			prepare: func(t *testLogicalSwitchPort) {
+			},
+			err: true,
+		},
 	}
 	for _, tt := range test {
 		t.Run(fmt.Sprintf("ApiUpdate: %s", tt.name), func(t *testing.T) {
@@ -1268,13 +1462,15 @@ func TestAPIUpdate(t *testing.T) {
 			cond := tt.condition(api)
 			// clean test Object
 			testObj = testLogicalSwitchPort{}
-			tt.prepare(&testObj)
+			if tt.prepare != nil {
+				tt.prepare(&testObj)
+			}
 			ops, err := cond.Update(&testObj, tt.fields...)
 			if tt.err {
-				assert.NotNil(t, err)
+				require.Error(t, err)
 			} else {
-				assert.Nil(t, err)
-				assert.ElementsMatchf(t, tt.result, ops, "ovsdb.Operations should match")
+				require.NoError(t, err)
+				require.ElementsMatchf(t, tt.result, ops, "ovsdb.Operations should match")
 			}
 		})
 	}
@@ -1371,7 +1567,7 @@ func TestAPIDelete(t *testing.T) {
 				t := testLogicalSwitchPort{
 					Enabled: &trueVal,
 				}
-				return a.Where(&t, model.Condition{
+				return a.WhereAny(&t, model.Condition{
 					Field:    &t.Type,
 					Function: ovsdb.ConditionEqual,
 					Value:    "sometype",
@@ -1392,7 +1588,7 @@ func TestAPIDelete(t *testing.T) {
 				t := testLogicalSwitchPort{
 					Enabled: &trueVal,
 				}
-				return a.Where(&t,
+				return a.WhereAny(&t,
 					model.Condition{
 						Field:    &t.Type,
 						Function: ovsdb.ConditionEqual,
@@ -1467,6 +1663,24 @@ func TestAPIDelete(t *testing.T) {
 			},
 			err: false,
 		},
+		{
+			name: "multiple different selected models must fail",
+			condition: func(a API) ConditionalAPI {
+				return a.Where(
+					&testLogicalSwitchPort{UUID: aUUID0},
+					&testLogicalSwitchPort{UUID: aUUID1},
+					&testLogicalSwitch{UUID: aUUID2},
+				)
+			},
+			err: true,
+		},
+		{
+			name: "fails if conditional is an error",
+			condition: func(a API) ConditionalAPI {
+				return newConditionalAPI(nil, newErrorConditional(fmt.Errorf("error")), &discardLogger)
+			},
+			err: true,
+		},
 	}
 	for _, tt := range test {
 		t.Run(fmt.Sprintf("ApiDelete: %s", tt.name), func(t *testing.T) {
@@ -1474,9 +1688,9 @@ func TestAPIDelete(t *testing.T) {
 			cond := tt.condition(api)
 			ops, err := cond.Delete()
 			if tt.err {
-				assert.NotNil(t, err)
+				assert.Error(t, err)
 			} else {
-				assert.Nil(t, err)
+				assert.NoError(t, err)
 				assert.ElementsMatchf(t, tt.result, ops, "ovsdb.Operations should match")
 			}
 		})
@@ -1560,6 +1774,71 @@ func BenchmarkAPIList(b *testing.B) {
 	}
 }
 
+func BenchmarkAPIListMultiple(b *testing.B) {
+	const numRows = 500
+
+	lscacheList := make([]*testLogicalSwitchPort, 0, numRows)
+
+	for i := 0; i < numRows; i++ {
+		lscacheList = append(lscacheList,
+			&testLogicalSwitchPort{
+				UUID:        uuid.New().String(),
+				Name:        fmt.Sprintf("ls%d", i),
+				ExternalIds: map[string]string{"foo": "bar"},
+			})
+	}
+	lscache := map[string]model.Model{}
+	for i := range lscacheList {
+		lscache[lscacheList[i].UUID] = lscacheList[i]
+	}
+	testData := cache.Data{
+		"Logical_Switch_Port": lscache,
+	}
+	tcache := apiTestCache(b, testData)
+
+	models := make([]model.Model, len(lscacheList))
+	for i := 0; i < len(lscacheList); i++ {
+		models[i] = &testLogicalSwitchPort{UUID: lscacheList[i].UUID}
+	}
+
+	test := []struct {
+		name     string
+		whereAny bool
+	}{
+		{
+			name: "multiple results one at a time with Get",
+		},
+		{
+			name:     "multiple results in a batch with WhereAny",
+			whereAny: true,
+		},
+	}
+
+	for _, tt := range test {
+		b.Run(tt.name, func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				var results []*testLogicalSwitchPort
+				api := newAPI(tcache, &discardLogger)
+				if tt.whereAny {
+					// Looking up models with WhereAny() should be fast
+					cond := api.Where(models...)
+					err := cond.List(context.Background(), &results)
+					assert.NoError(b, err)
+				} else {
+					// Looking up models one-at-a-time with Get() should be slow
+					for j := 0; j < len(lscacheList); j++ {
+						m := &testLogicalSwitchPort{UUID: lscacheList[j].UUID}
+						err := api.Get(context.Background(), m)
+						assert.NoError(b, err)
+						results = append(results, m)
+					}
+				}
+				assert.Len(b, results, len(models))
+			}
+		})
+	}
+}
+
 func TestAPIWait(t *testing.T) {
 	tcache := apiTestCache(t, cache.Data{})
 	timeout0 := 0
@@ -1646,7 +1925,7 @@ func TestAPIWait(t *testing.T) {
 						Value:    "lspNameCondition",
 					},
 				}
-				return a.Where(&lsp, conditions...)
+				return a.WhereAny(&lsp, conditions...)
 			},
 			until: "!=",
 			prepare: func() (model.Model, []interface{}) {
@@ -1710,6 +1989,16 @@ func TestAPIWait(t *testing.T) {
 			result: []ovsdb.Operation{},
 			err:    false,
 		},
+		{
+			name: "fails if conditional is an error",
+			condition: func(a API) ConditionalAPI {
+				return newConditionalAPI(nil, newErrorConditional(fmt.Errorf("error")), &discardLogger)
+			},
+			prepare: func() (model.Model, []interface{}) {
+				return nil, nil
+			},
+			err: true,
+		},
 	}
 
 	for _, tt := range test {
diff --git a/client/condition_test.go b/client/condition_test.go
index 6e0be86..5bd63b3 100644
--- a/client/condition_test.go
+++ b/client/condition_test.go
@@ -48,14 +48,16 @@ func TestEqualityConditional(t *testing.T) {
 
 	test := []struct {
 		name      string
-		model     model.Model
+		models    []model.Model
 		condition [][]ovsdb.Condition
 		matches   map[string]model.Model
 		err       bool
 	}{
 		{
-			name:  "by uuid",
-			model: &testLogicalSwitchPort{UUID: aUUID0, Name: "different"},
+			name: "by uuid",
+			models: []model.Model{
+				&testLogicalSwitchPort{UUID: aUUID0, Name: "different"},
+			},
 			condition: [][]ovsdb.Condition{
 				{
 					{
@@ -66,37 +68,89 @@ func TestEqualityConditional(t *testing.T) {
 			matches: map[string]model.Model{aUUID0: lspcacheList[0]},
 		},
 		{
-			name:  "by index with cache",
-			model: &testLogicalSwitchPort{Name: "lsp1"},
+			name: "by uuids",
+			models: []model.Model{
+				&testLogicalSwitchPort{UUID: aUUID0, Name: "different"},
+				&testLogicalSwitchPort{UUID: aUUID1, Name: "different2"},
+			},
 			condition: [][]ovsdb.Condition{
-				{
-					{
-						Column:   "_uuid",
-						Function: ovsdb.ConditionEqual,
-						Value:    ovsdb.UUID{GoUUID: aUUID1},
-					}}},
-			matches: map[string]model.Model{aUUID1: lspcacheList[1]},
+				{{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID0},
+				}},
+				{{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID1},
+				}},
+			},
+			matches: map[string]model.Model{
+				aUUID0: lspcacheList[0],
+				aUUID1: lspcacheList[1],
+			},
 		},
 		{
-			name:  "by index with no cache",
-			model: &testLogicalSwitchPort{Name: "foo"},
+			name: "by index with cache",
+			models: []model.Model{
+				&testLogicalSwitchPort{Name: "lsp1"},
+				&testLogicalSwitchPort{Name: "lsp2"},
+			},
 			condition: [][]ovsdb.Condition{
-				{
-					{
-						Column:   "name",
-						Function: ovsdb.ConditionEqual,
-						Value:    "foo",
-					}}},
+				{{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID1},
+				}},
+				{{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID2},
+				}},
+			},
+			matches: map[string]model.Model{
+				aUUID1: lspcacheList[1],
+				aUUID2: lspcacheList[2],
+			},
 		},
 		{
-			name:  "by non index",
-			model: &testLogicalSwitchPort{ExternalIds: map[string]string{"foo": "baz"}},
-			err:   true,
+			name: "by index with no cache",
+			models: []model.Model{
+				&testLogicalSwitchPort{Name: "foo"},
+				&testLogicalSwitchPort{Name: "123"},
+			},
+			condition: [][]ovsdb.Condition{
+				{{
+					Column:   "name",
+					Function: ovsdb.ConditionEqual,
+					Value:    "foo",
+				}},
+				{{
+					Column:   "name",
+					Function: ovsdb.ConditionEqual,
+					Value:    "123",
+				}},
+			},
+		},
+		{
+			name: "by non index",
+			models: []model.Model{
+				&testLogicalSwitchPort{ExternalIds: map[string]string{"foo": "baz"}},
+			},
+			err: true,
+		},
+		{
+			name: "by non index multiple models",
+			models: []model.Model{
+				&testLogicalSwitchPort{ExternalIds: map[string]string{"foo": "baz"}},
+				&testLogicalSwitchPort{ExternalIds: map[string]string{"foo": "123"}},
+			},
+			err: true,
 		},
 	}
 	for _, tt := range test {
 		t.Run(fmt.Sprintf("Equality Conditional: %s", tt.name), func(t *testing.T) {
-			cond, err := newEqualityConditional("Logical_Switch_Port", tcache, tt.model)
+			cond, err := newEqualityConditional("Logical_Switch_Port", tcache, tt.models)
 			assert.Nil(t, err)
 			matches, err := cond.Matches()
 			assert.Nil(t, err)
diff --git a/test/ovs/ovs_integration_test.go b/test/ovs/ovs_integration_test.go
index e6d8790..7f1b012 100644
--- a/test/ovs/ovs_integration_test.go
+++ b/test/ovs/ovs_integration_test.go
@@ -763,7 +763,7 @@ func (suite *OVSIntegrationSuite) TestWait() {
 		},
 	}
 	timeout := 0
-	ops, err := suite.client.Where(bridgeRow, conditions...).Wait(
+	ops, err := suite.client.WhereAny(bridgeRow, conditions...).Wait(
 		ovsdb.WaitConditionNotEqual, &timeout, bridgeRow, &bridgeRow.Name)
 	require.NoError(suite.T(), err)
 	reply, err := suite.client.Transact(context.Background(), ops...)
@@ -788,7 +788,7 @@ func (suite *OVSIntegrationSuite) TestWait() {
 		},
 	}
 	timeout = 2 * 1000 // 2 seconds (in milliseconds)
-	ops, err = suite.client.Where(bridgeRow, conditions...).Wait(
+	ops, err = suite.client.WhereAny(bridgeRow, conditions...).Wait(
 		ovsdb.WaitConditionEqual, &timeout, bridgeRow, &bridgeRow.BridgeFailMode)
 	require.NoError(suite.T(), err)
 	reply, err = suite.client.Transact(context.Background(), ops...)
@@ -798,7 +798,7 @@ func (suite *OVSIntegrationSuite) TestWait() {
 
 	// Use wait to get a txn error due to until condition that is not happening
 	timeout = 222 // milliseconds
-	ops, err = suite.client.Where(bridgeRow, conditions...).Wait(
+	ops, err = suite.client.WhereAny(bridgeRow, conditions...).Wait(
 		ovsdb.WaitConditionNotEqual, &timeout, bridgeRow, &bridgeRow.BridgeFailMode)
 	require.NoError(suite.T(), err)
 	reply, err = suite.client.Transact(context.Background(), ops...)