Repository: ovn-org/libovsdb
Pull request #113 (2021-05-17 07:48:43)

Add Explicit Conditions
They allow to express more complex conditions that will be evaluated at the server side. Since they include the field selection in the current conditionFromModel, reuse this way of creating conditions, e.g:

// No conditions falls back to equality, index-based conditions
```
api.Where(api.ConditionFromModel(&LogicalSwitch{Name:"foo"})).Update(...)
```
// Now we accept explicit conditions
```
ls := &LogicalSwitch{}
api.Where(api.ConditionFromModel(ls, Condition{
    Field: &ls.Enabled,
    Function: ovsdb.ConditionEqual,
    Value: []bool{true},
    })).Update(...)
```
Any number of conditions can be created to select multiple rows
Fixes: #99 



Patch:
diff --git a/client/api.go b/client/api.go
index 19ed60a..84c7f89 100644
--- a/client/api.go
+++ b/client/api.go
@@ -26,15 +26,15 @@ type API interface {
 	// Create a Condition from a Function that is used to filter cached data
 	// The function must accept a Model implementation and return a boolean. E.g:
 	// ConditionFromFunc(func(l *LogicalSwitch) bool { return l.Enabled })
-	ConditionFromFunc(predicate interface{}) Condition
+	ConditionFromFunc(predicate interface{}) Conditional
 
 	// Create a Condition from a Model's data. It uses the database indexes
 	// to search the most apropriate field to use for matches and conditions
 	// Optionally, a list of fields can indicate an alternative index
-	ConditionFromModel(Model, ...interface{}) Condition
+	ConditionFromModel(Model, ...Condition) Conditional
 
 	// Create a ConditionalAPI from a Condition
-	Where(condition Condition) ConditionalAPI
+	Where(condition Conditional) ConditionalAPI
 
 	// Get retrieves a model from the cache
 	// The way the object will be fetch depends on the data contained in the
@@ -82,6 +82,16 @@ type Mutation struct {
 	Value interface{}
 }
 
+// Condition is a type that represents a OVSDB Condition
+type Condition struct {
+	// Pointer to the field of the model where the operation applies
+	Field interface{}
+	// Condition function
+	Function ovsdb.ConditionFunction
+	// Value to use in the condition
+	Value interface{}
+}
+
 // InputTypeError is used to report the user provided parameter has the wrong type
 type InputTypeError struct {
 	inputType reflect.Type
@@ -112,7 +122,7 @@ var ErrNotFound = errors.New("object not found")
 // Where() can be used to create a ConditionalAPI api
 type api struct {
 	cache *TableCache
-	cond  Condition
+	cond  Conditional
 }
 
 // List populates a slice of Models given as parameter based on the configured Condition
@@ -170,36 +180,48 @@ func (a api) List(result interface{}) error {
 }
 
 // Where returns a conditionalAPI based a Condition
-func (a api) Where(condition Condition) ConditionalAPI {
+func (a api) Where(condition Conditional) ConditionalAPI {
 	return newConditionalAPI(a.cache, condition)
 }
 
-// ConditionFactory interface implementation
+// Conditional interface implementation
 // FromFunc returns a Condition from a function
-func (a api) ConditionFromFunc(predicate interface{}) Condition {
+func (a api) ConditionFromFunc(predicate interface{}) Conditional {
 	table, err := a.getTableFromFunc(predicate)
 	if err != nil {
-		return newErrorCondition(err)
+		return newErrorConditional(err)
 	}
 
-	condition, err := newPredicateCond(table, a.cache, predicate)
+	condition, err := newPredicateConditional(table, a.cache, predicate)
 	if err != nil {
-		return newErrorCondition(err)
+		return newErrorConditional(err)
 	}
 	return condition
 }
 
 // FromModel returns a Condition from a model and a list of fields
-func (a api) ConditionFromModel(model Model, fields ...interface{}) Condition {
+func (a api) ConditionFromModel(model Model, cond ...Condition) Conditional {
+	var conditional Conditional
+	var err error
+
 	tableName, err := a.getTableFromModel(model)
 	if tableName == "" {
-		return newErrorCondition(err)
+		return newErrorConditional(err)
 	}
-	condition, err := newIndexCondition(a.cache.orm, tableName, model, fields...)
-	if err != nil {
-		return newErrorCondition(err)
+
+	if len(cond) == 0 {
+		conditional, err = newEqualityConditional(a.cache.orm, tableName, model)
+		if err != nil {
+			conditional = newErrorConditional(err)
+		}
+
+	} else {
+		conditional, err = newExplicitConditional(a.cache.orm, tableName, model, cond...)
+		if err != nil {
+			conditional = newErrorConditional(err)
+		}
 	}
-	return condition
+	return conditional
 }
 
 // Get is a generic Get function capable of returning (through a provided pointer)
@@ -439,7 +461,7 @@ func newAPI(cache *TableCache) API {
 }
 
 // newConditionalAPI returns a new ConditionalAPI to interact with the database
-func newConditionalAPI(cache *TableCache, cond Condition) ConditionalAPI {
+func newConditionalAPI(cache *TableCache, cond Conditional) ConditionalAPI {
 	return api{
 		cache: cache,
 		cond:  cond,
diff --git a/client/client.go b/client/client.go
index dbb5777..a234d02 100644
--- a/client/client.go
+++ b/client/client.go
@@ -371,16 +371,16 @@ func (ovs OvsdbClient) List(result interface{}) error {
 }
 
 //Where implements the API interface's Where function
-func (ovs OvsdbClient) Where(condition Condition) ConditionalAPI {
+func (ovs OvsdbClient) Where(condition Conditional) ConditionalAPI {
 	return ovs.api.Where(condition)
 }
 
 //ConditionFromFunc implements the API interface's ConditionFromFunc function
-func (ovs OvsdbClient) ConditionFromFunc(predicate interface{}) Condition {
+func (ovs OvsdbClient) ConditionFromFunc(predicate interface{}) Conditional {
 	return ovs.api.ConditionFromFunc(predicate)
 }
 
 //ConditionFromModel implements the API interface's ConditionFromModel function
-func (ovs OvsdbClient) ConditionFromModel(m Model, fields ...interface{}) Condition {
-	return ovs.api.ConditionFromModel(m, fields...)
+func (ovs OvsdbClient) ConditionFromModel(m Model, conditions ...Condition) Conditional {
+	return ovs.api.ConditionFromModel(m, conditions...)
 }
diff --git a/client/condition.go b/client/condition.go
index 7f11f18..452f501 100644
--- a/client/condition.go
+++ b/client/condition.go
@@ -7,9 +7,9 @@ import (
 	"github.com/ovn-org/libovsdb/ovsdb"
 )
 
-// Condition is the interface used by the ConditionalAPI to match on cache objects
-// and generate operation conditions
-type Condition interface {
+// Conditional is the interface used by the ConditionalAPI to match on cache objects
+// and generate ovsdb conditions
+type Conditional interface {
 	// Generate returns a list of conditions to be used in Operations
 	Generate() ([]ovsdb.Condition, error)
 	// matches returns true if a model matches the condition
@@ -18,46 +18,83 @@ type Condition interface {
 	Table() string
 }
 
-// indexCond uses the information available in a model to generate conditions
+// equalityConditional uses the information available in a model to generate conditions
 // The conditions are based on the equality of the first available index.
-// The priority of indexes is: {user_provided fields}, uuid, {schema index}
-type indexCond struct {
+// The priority of indexes is: uuid, {schema index}
+type equalityConditional struct {
 	orm       *orm
 	tableName string
 	model     Model
-	fields    []interface{}
 }
 
-func (c *indexCond) Matches(m Model) (bool, error) {
-	return c.orm.equalFields(c.tableName, c.model, m, c.fields...)
+func (c *equalityConditional) Matches(m Model) (bool, error) {
+	return c.orm.equalFields(c.tableName, c.model, m)
 }
 
-func (c *indexCond) Table() string {
+func (c *equalityConditional) Table() string {
 	return c.tableName
 }
 
 // Generate returns a condition based on the model and the field pointers
-func (c *indexCond) Generate() ([]ovsdb.Condition, error) {
-	condition, err := c.orm.newCondition(c.tableName, c.model, c.fields...)
+func (c *equalityConditional) Generate() ([]ovsdb.Condition, error) {
+	condition, err := c.orm.newEqualityCondition(c.tableName, c.model)
 	if err != nil {
 		return nil, err
 	}
 	return condition, nil
 }
 
-// newIndexCondition creates a new indexCond
-func newIndexCondition(orm *orm, table string, model Model, fields ...interface{}) (Condition, error) {
-	return &indexCond{
+// newIndexCondition creates a new equalityConditional
+func newEqualityConditional(orm *orm, table string, model Model, fields ...interface{}) (Conditional, error) {
+	return &equalityConditional{
 		orm:       orm,
 		tableName: table,
 		model:     model,
-		fields:    fields,
 	}, nil
 }
 
-// predicateCond is a conditionFactory that calls a provided function pointer
+// explicitConditional generates conditions based on the provided Condition list
+type explicitConditional struct {
+	orm        *orm
+	tableName  string
+	model      Model
+	conditions []Condition
+}
+
+func (c *explicitConditional) Matches(m Model) (bool, error) {
+	return false, fmt.Errorf("Cannot perform Cache comparisons using explicit Conditions")
+}
+
+func (c *explicitConditional) Table() string {
+	return c.tableName
+}
+
+// Generate returns a condition based on the model and the field pointers
+func (c *explicitConditional) Generate() ([]ovsdb.Condition, error) {
+	var result []ovsdb.Condition
+	for _, cond := range c.conditions {
+		ovsdbCond, err := c.orm.newCondition(c.tableName, c.model, cond)
+		if err != nil {
+			return nil, err
+		}
+		result = append(result, *ovsdbCond)
+	}
+	return result, nil
+}
+
+// newIndexCondition creates a new equalityConditional
+func newExplicitConditional(orm *orm, table string, model Model, cond ...Condition) (Conditional, error) {
+	return &explicitConditional{
+		orm:        orm,
+		tableName:  table,
+		model:      model,
+		conditions: cond,
+	}, nil
+}
+
+// predicateConditional is a Conditional that calls a provided function pointer
 // to match on models.
-type predicateCond struct {
+type predicateConditional struct {
 	tableName string
 	predicate interface{}
 	cache     *TableCache
@@ -65,18 +102,18 @@ type predicateCond struct {
 
 // matches returns the result of the execution of the predicate
 // Type verifications are not performed
-func (c *predicateCond) Matches(model Model) (bool, error) {
+func (c *predicateConditional) Matches(model Model) (bool, error) {
 	ret := reflect.ValueOf(c.predicate).Call([]reflect.Value{reflect.ValueOf(model)})
 	return ret[0].Bool(), nil
 }
 
-func (c *predicateCond) Table() string {
+func (c *predicateConditional) Table() string {
 	return c.tableName
 }
 
 // generate returns a list of conditions that match, by _uuid equality, all the objects that
 // match the predicate
-func (c *predicateCond) Generate() ([]ovsdb.Condition, error) {
+func (c *predicateConditional) Generate() ([]ovsdb.Condition, error) {
 	allConditions := make([]ovsdb.Condition, 0)
 	tableCache := c.cache.Table(c.tableName)
 	if tableCache == nil {
@@ -89,7 +126,7 @@ func (c *predicateCond) Generate() ([]ovsdb.Condition, error) {
 			return nil, err
 		}
 		if match {
-			elemCond, err := c.cache.orm.newCondition(c.tableName, elem)
+			elemCond, err := c.cache.orm.newEqualityCondition(c.tableName, elem)
 			if err != nil {
 				return nil, err
 			}
@@ -99,35 +136,35 @@ func (c *predicateCond) Generate() ([]ovsdb.Condition, error) {
 	return allConditions, nil
 }
 
-// newIndexCondition creates a new predicateCond
-func newPredicateCond(table string, cache *TableCache, predicate interface{}) (Condition, error) {
-	return &predicateCond{
+// newPredicateConditional creates a new predicateConditional
+func newPredicateConditional(table string, cache *TableCache, predicate interface{}) (Conditional, error) {
+	return &predicateConditional{
 		tableName: table,
 		predicate: predicate,
 		cache:     cache,
 	}, nil
 }
 
-// errorCondition is a condition that encapsulates an error
-// It is used to delay the reporting of errors from condition creation to method call
-type errorCondition struct {
+// errorConditional is a conditional that encapsulates an error
+// It is used to delay the reporting of errors from conditional creation to API method call
+type errorConditional struct {
 	err error
 }
 
-func (e *errorCondition) Matches(Model) (bool, error) {
+func (e *errorConditional) Matches(Model) (bool, error) {
 	return false, e.err
 }
 
-func (e *errorCondition) Table() string {
+func (e *errorConditional) Table() string {
 	return ""
 }
 
-func (e *errorCondition) Generate() ([]ovsdb.Condition, error) {
+func (e *errorConditional) Generate() ([]ovsdb.Condition, error) {
 	return nil, e.err
 }
 
-func newErrorCondition(err error) Condition {
-	return &errorCondition{
+func newErrorConditional(err error) Conditional {
+	return &errorConditional{
 		err: fmt.Errorf("conditionerror: %s", err.Error()),
 	}
 }
diff --git a/client/orm.go b/client/orm.go
index 72dc3f5..0614b1e 100644
--- a/client/orm.go
+++ b/client/orm.go
@@ -158,7 +158,7 @@ func (o orm) newRow(tableName string, data interface{}, fields ...interface{}) (
 	return ovsRow, nil
 }
 
-// newCondition returns a list of conditions that match a given object
+// newEquality Condition returns a list of equality conditions that match a given object
 // A list of valid columns that shall be used as a index can be provided.
 // If none are provided, we will try to use object's field that matches the '_uuid' ovs tag
 // If it does not exist or is null (""), then we will traverse all of the table indexes and
@@ -166,7 +166,7 @@ func (o orm) newRow(tableName string, data interface{}, fields ...interface{}) (
 // object has valid data. The order in which they are traversed matches the order defined
 // in the schema.
 // By `valid data` we mean non-default data.
-func (o orm) newCondition(tableName string, data interface{}, fields ...interface{}) ([]ovsdb.Condition, error) {
+func (o orm) newEqualityCondition(tableName string, data interface{}, fields ...interface{}) ([]ovsdb.Condition, error) {
 	var conditions []ovsdb.Condition
 	var condIndex [][]string
 
@@ -248,6 +248,43 @@ func (o orm) equalFields(tableName string, one, other interface{}, fields ...int
 	return o.equalIndexes(table, one, other, indexes...)
 }
 
+// newCondition returns a ovsdb.Condition based on a client.Condition
+func (o orm) newCondition(tableName string, data interface{}, condition Condition) (*ovsdb.Condition, error) {
+	table := o.schema.Table(tableName)
+	if table == nil {
+		return nil, NewErrNoTable(tableName)
+	}
+
+	ormInfo, err := newORMInfo(table, data)
+	if err != nil {
+		return nil, err
+	}
+
+	column, err := ormInfo.columnByPtr(condition.Field)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check that the condition is valid
+	columnSchema := table.Column(column)
+	if columnSchema == nil {
+		return nil, fmt.Errorf("column %s not found", column)
+	}
+	if err := ovsdb.ValidateCondition(columnSchema, condition.Function, condition.Value); err != nil {
+		return nil, err
+	}
+
+	ovsValue, err := ovsdb.NativeToOvs(columnSchema, condition.Value)
+	if err != nil {
+		return nil, err
+	}
+
+	ovsdbCondition := ovsdb.NewCondition(column, condition.Function, ovsValue)
+
+	return &ovsdbCondition, nil
+
+}
+
 // newMutation creates a RFC7047 mutation object based on an ORM object and the mutation fields (in native format)
 // It takes care of field validation against the column type
 func (o orm) newMutation(tableName string, data interface{}, column string, mutator ovsdb.Mutator, value interface{}) ([]interface{}, error) {
diff --git a/cmd/modelgen/dbmodel.go b/cmd/modelgen/dbmodel.go
index 9c58fab..2bd8603 100644
--- a/cmd/modelgen/dbmodel.go
+++ b/cmd/modelgen/dbmodel.go
@@ -16,7 +16,7 @@ import (
 	"github.com/ovn-org/libovsdb/client"
 )
 
-// FullDatabaseModel returns the DatabaseModel object to be used in libovsdb
+// FullDatabaseModel() returns the DatabaseModel object to be used in libovsdb
 func FullDatabaseModel() (*client.DBModel, error) {
 	return client.NewDBModel("{{ .DatabaseName }}", map[string]client.Model{
     {{ range $tableName, $structName := .Tables }} "{{ $tableName }}" : &{{ $structName }}{}, 
diff --git a/cmd/modelgen/table.go b/cmd/modelgen/table.go
index 97b796c..4a92383 100644
--- a/cmd/modelgen/table.go
+++ b/cmd/modelgen/table.go
@@ -5,6 +5,7 @@ import (
 	"sort"
 	"strings"
 	"text/template"
+	"unicode"
 
 	"github.com/ovn-org/libovsdb/ovsdb"
 )
@@ -126,60 +127,14 @@ func FileName(table string) string {
 	return fmt.Sprintf("%s.go", strings.ToLower(table))
 }
 
-// common initialisms used in ovsdb schemas
-var initialisms = map[string]bool{
-	"ACL":   true,
-	"BFD":   true,
-	"CFM":   true,
-	"CT":    true,
-	"CVLAN": true,
-	"DNS":   true,
-	"DSCP":  true,
-	"ID":    true,
-	"IP":    true,
-	"IPFIX": true,
-	"LACP":  true,
-	"LLDP":  true,
-	"MAC":   true,
-	"MTU":   true,
-	"OVS":   true,
-	"QOS":   true,
-	"RSTP":  true,
-	"SSL":   true,
-	"STP":   true,
-	"TCP":   true,
-	"UDP":   true,
-	"UUID":  true,
-	"VLAN":  true,
-}
-
 func camelCase(field string) string {
-	s := strings.ToLower(field)
-	parts := strings.FieldsFunc(s, func(r rune) bool {
-		return r == '_' || r == '-'
-	})
-	if len(parts) > 1 {
-		s = ""
-		for _, p := range parts {
-			s += strings.Title(expandInitilaisms(p))
-		}
-	} else {
-		s = strings.Title(expandInitilaisms(s))
-	}
-	return s
-}
-
-func expandInitilaisms(s string) string {
-	// check initialisms
-	if u := strings.ToUpper(s); initialisms[u] {
-		return strings.ToUpper(s)
-	}
-	// check for plurals too
-	if strings.HasSuffix(s, "s") {
-		sub := s[:len(s)-1]
-		if u := strings.ToUpper(sub); initialisms[u] {
-			return strings.ToUpper(sub) + "s"
+	capNext := true
+	orig := []rune(field)
+	for i, c := range orig {
+		if capNext {
+			orig[i] = unicode.ToUpper(c)
 		}
+		capNext = c == '_' || c == '-'
 	}
-	return s
+	return strings.ReplaceAll(string(orig), "_", "")
 }
diff --git a/ovsdb/bindings.go b/ovsdb/bindings.go
index 3797761..8a9f73d 100644
--- a/ovsdb/bindings.go
+++ b/ovsdb/bindings.go
@@ -294,6 +294,28 @@ func ValidateMutation(column *ColumnSchema, mutator Mutator, value interface{})
 	}
 }
 
+func ValidateCondition(column *ColumnSchema, function ConditionFunction, nativeValue interface{}) error {
+	if NativeType(column) != reflect.TypeOf(nativeValue) {
+		return NewErrWrongType(fmt.Sprintf("Condition for column %s", column),
+			NativeType(column).String(), nativeValue)
+	}
+
+	switch column.Type {
+	case TypeSet, TypeMap, TypeBoolean, TypeString, TypeUUID:
+		switch function {
+		case ConditionEqual, ConditionNotEqual, ConditionIncludes, ConditionExcludes:
+			return nil
+		default:
+			return fmt.Errorf("wrong condition function %s for type: %s", function, column.Type)
+		}
+	case TypeInteger, TypeReal:
+		// All functions are valid
+		return nil
+	default:
+		panic("Unsupported Type")
+	}
+}
+
 func isDefaultBaseValue(elem interface{}, etype ExtendedType) bool {
 	value := reflect.ValueOf(elem)
 	if !value.IsValid() {


Test patch:
diff --git a/client/api_test.go b/client/api_test.go
index ed45baf..68987c2 100644
--- a/client/api_test.go
+++ b/client/api_test.go
@@ -275,24 +275,6 @@ func TestAPIListFields(t *testing.T) {
 			content: []Model{lspcache[aUUID2]},
 			err:     false,
 		},
-		{
-			name: "List unique by extra field",
-			prepare: func(t *testLogicalSwitchPort) {
-				t.ExternalIds = map[string]string{"unique": "id"}
-			},
-			content: []Model{lspcache[aUUID2]},
-			fields:  []interface{}{&testObj.ExternalIds},
-			err:     false,
-		},
-		{
-			name: "List by extra field",
-			prepare: func(t *testLogicalSwitchPort) {
-				t.Enabled = []bool{true}
-			},
-			content: []Model{lspcache[aUUID0], lspcache[aUUID3]},
-			fields:  []interface{}{&testObj.Enabled},
-			err:     false,
-		},
 	}
 
 	for _, tt := range test {
@@ -301,7 +283,7 @@ func TestAPIListFields(t *testing.T) {
 			// Clean object
 			testObj = testLogicalSwitchPort{}
 			api := newAPI(cache)
-			err := api.Where(api.ConditionFromModel(&testObj, tt.fields...)).List(&result)
+			err := api.Where(api.ConditionFromModel(&testObj)).List(&result)
 			if tt.err {
 				assert.NotNil(t, err)
 			} else {
@@ -322,18 +304,6 @@ func TestAPIListFields(t *testing.T) {
 		err := api.Where(api.ConditionFromModel(&obj)).List(&result)
 		assert.NotNil(t, err)
 	})
-
-	t.Run("ApiListFields: Wrong object field", func(t *testing.T) {
-		var result []testLogicalSwitchPort
-		api := newAPI(cache)
-		obj := testLogicalSwitch{}
-		obj2 := testLogicalSwitch{
-			UUID: aUUID0,
-		}
-
-		err := api.Where(api.ConditionFromModel(&obj, &obj2.UUID)).List(&result)
-		assert.NotNil(t, err)
-	})
 }
 
 func TestConditionFromFunc(t *testing.T) {
@@ -371,9 +341,9 @@ func TestConditionFromFunc(t *testing.T) {
 			api := newAPI(cache)
 			condition := api.ConditionFromFunc(tt.arg)
 			if tt.err {
-				assert.IsType(t, &errorCondition{}, condition)
+				assert.IsType(t, &errorConditional{}, condition)
 			} else {
-				assert.IsType(t, &predicateCond{}, condition)
+				assert.IsType(t, &predicateConditional{}, condition)
 			}
 		})
 	}
@@ -382,10 +352,10 @@ func TestConditionFromFunc(t *testing.T) {
 func TestConditionFromModel(t *testing.T) {
 	var testObj testLogicalSwitch
 	test := []struct {
-		name   string
-		model  Model
-		fields []interface{}
-		err    bool
+		name  string
+		model Model
+		conds []Condition
+		err   bool
 	}{
 		{
 			name:  "wrong model must fail",
@@ -393,12 +363,12 @@ func TestConditionFromModel(t *testing.T) {
 			err:   true,
 		},
 		{
-			name: "wrong fields must fail",
+			name: "wrong condition must fail",
 			model: &struct {
 				a string `ovs:"_uuid"`
 			}{},
-			fields: []interface{}{"foo"},
-			err:    true,
+			conds: []Condition{{Field: "foo"}},
+			err:   true,
 		},
 		{
 			name:  "correct model must succeed",
@@ -406,10 +376,21 @@ func TestConditionFromModel(t *testing.T) {
 			err:   false,
 		},
 		{
-			name:   "correct model with fields must succeed",
-			model:  &testObj,
-			fields: []interface{}{&testObj.Name},
-			err:    false,
+			name:  "correct model with valid condition must succeed",
+			model: &testObj,
+			conds: []Condition{
+				{
+					Field:    &testObj.Name,
+					Function: ovsdb.ConditionEqual,
+					Value:    "foo",
+				},
+				{
+					Field:    &testObj.Ports,
+					Function: ovsdb.ConditionIncludes,
+					Value:    []string{"foo"},
+				},
+			},
+			err: false,
 		},
 	}
 
@@ -417,11 +398,16 @@ func TestConditionFromModel(t *testing.T) {
 		t.Run(fmt.Sprintf("ConditionFromModel: %s", tt.name), func(t *testing.T) {
 			cache := apiTestCache(t)
 			api := newAPI(cache)
-			condition := api.ConditionFromModel(tt.model, tt.fields...)
+			condition := api.ConditionFromModel(tt.model, tt.conds...)
 			if tt.err {
-				assert.IsType(t, &errorCondition{}, condition)
+				assert.IsType(t, &errorConditional{}, condition)
 			} else {
-				assert.IsType(t, &indexCond{}, condition)
+				if len(tt.conds) > 0 {
+					assert.IsType(t, &explicitConditional{}, condition)
+				} else {
+					assert.IsType(t, &equalityConditional{}, condition)
+				}
+
 			}
 		})
 	}
@@ -810,7 +796,6 @@ func TestAPIUpdate(t *testing.T) {
 		name      string
 		condition func(API) ConditionalAPI
 		prepare   func(t *testLogicalSwitchPort)
-		fields    []interface{}
 		result    []ovsdb.Operation
 		err       bool
 	}{
@@ -825,7 +810,6 @@ func TestAPIUpdate(t *testing.T) {
 				t.Type = "somethingElse"
 				t.Tag = []int{6}
 			},
-			fields: []interface{}{},
 			result: []ovsdb.Operation{
 				{
 					Op:    opUpdate,
@@ -847,7 +831,6 @@ func TestAPIUpdate(t *testing.T) {
 				t.Type = "somethingElse"
 				t.Tag = []int{6}
 			},
-			fields: []interface{}{},
 			result: []ovsdb.Operation{
 				{
 					Op:    opUpdate,
@@ -865,12 +848,15 @@ func TestAPIUpdate(t *testing.T) {
 					Type:    "sometype",
 					Enabled: []bool{true},
 				}
-				return a.Where(a.ConditionFromModel(&t, &t.Type))
+				return a.Where(a.ConditionFromModel(&t, Condition{
+					Field:    &t.Type,
+					Function: ovsdb.ConditionEqual,
+					Value:    "sometype",
+				}))
 			},
 			prepare: func(t *testLogicalSwitchPort) {
 				t.Tag = []int{6}
 			},
-			fields: []interface{}{},
 			result: []ovsdb.Operation{
 				{
 					Op:    opUpdate,
@@ -881,6 +867,32 @@ func TestAPIUpdate(t *testing.T) {
 			},
 			err: false,
 		},
+		{
+			name: "select by field inequality change multiple field",
+			condition: func(a API) ConditionalAPI {
+				t := testLogicalSwitchPort{
+					Type:    "sometype",
+					Enabled: []bool{true},
+				}
+				return a.Where(a.ConditionFromModel(&t, Condition{
+					Field:    &t.Type,
+					Function: ovsdb.ConditionNotEqual,
+					Value:    "sometype",
+				}))
+			},
+			prepare: func(t *testLogicalSwitchPort) {
+				t.Tag = []int{6}
+			},
+			result: []ovsdb.Operation{
+				{
+					Op:    opUpdate,
+					Table: "Logical_Switch_Port",
+					Row:   map[string]interface{}{"tag": testOvsSet(t, []int{6})},
+					Where: []ovsdb.Condition{{Column: "type", Function: ovsdb.ConditionNotEqual, Value: "sometype"}},
+				},
+			},
+			err: false,
+		},
 		{
 			name: "select multiple by predicate change multiple field",
 			condition: func(a API) ConditionalAPI {
@@ -892,7 +904,6 @@ func TestAPIUpdate(t *testing.T) {
 				t.Type = "somethingElse"
 				t.Tag = []int{6}
 			},
-			fields: []interface{}{},
 			result: []ovsdb.Operation{
 				{
 					Op:    opUpdate,
@@ -996,13 +1007,16 @@ func TestAPIDelete(t *testing.T) {
 			err: false,
 		},
 		{
-			name: "select by field",
+			name: "select by field equality",
 			condition: func(a API) ConditionalAPI {
 				t := testLogicalSwitchPort{
-					Type:    "sometype",
 					Enabled: []bool{true},
 				}
-				return a.Where(a.ConditionFromModel(&t, &t.Type))
+				return a.Where(a.ConditionFromModel(&t, Condition{
+					Field:    &t.Type,
+					Function: ovsdb.ConditionEqual,
+					Value:    "sometype",
+				}))
 			},
 			result: []ovsdb.Operation{
 				{
diff --git a/client/condition_test.go b/client/condition_test.go
new file mode 100644
index 0000000..09e54ee
--- /dev/null
+++ b/client/condition_test.go
@@ -0,0 +1,348 @@
+package client
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/ovn-org/libovsdb/ovsdb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEqualityConditional(t *testing.T) {
+	cache := apiTestCache(t)
+	lspcacheList := []Model{
+		&testLogicalSwitchPort{
+			UUID:        aUUID0,
+			Name:        "lsp0",
+			ExternalIds: map[string]string{"foo": "bar"},
+			Enabled:     []bool{true},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID1,
+			Name:        "lsp1",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     []bool{false},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID2,
+			Name:        "lsp2",
+			ExternalIds: map[string]string{"unique": "id"},
+			Enabled:     []bool{false},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID3,
+			Name:        "lsp3",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     []bool{true},
+		},
+	}
+	lspcache := map[string]Model{}
+	for i := range lspcacheList {
+		lspcache[lspcacheList[i].(*testLogicalSwitchPort).UUID] = lspcacheList[i]
+	}
+	cache.cache["Logical_Switch_Port"] = &RowCache{cache: lspcache}
+
+	test := []struct {
+		name      string
+		model     Model
+		condition []ovsdb.Condition
+		matches   map[Model]bool
+		err       bool
+	}{
+		{
+			name:  "by uuid",
+			model: &testLogicalSwitchPort{UUID: aUUID0, Name: "different"},
+			condition: []ovsdb.Condition{
+				{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID0},
+				}},
+			matches: map[Model]bool{
+				&testLogicalSwitchPort{UUID: aUUID0}:              true,
+				&testLogicalSwitchPort{UUID: aUUID1}:              false,
+				&testLogicalSwitchPort{UUID: aUUID0, Name: "foo"}: true,
+			},
+		},
+		{
+			name:  "by index",
+			model: &testLogicalSwitchPort{Name: "lsp1"},
+			condition: []ovsdb.Condition{
+				{
+					Column:   "name",
+					Function: ovsdb.ConditionEqual,
+					Value:    "lsp1",
+				}},
+			matches: map[Model]bool{
+				&testLogicalSwitchPort{UUID: aUUID1}:               false,
+				&testLogicalSwitchPort{UUID: aUUID1, Name: "lsp1"}: true,
+				&testLogicalSwitchPort{UUID: aUUID0, Name: "lsp1"}: true,
+			},
+		},
+		{
+			name:  "by non index",
+			model: &testLogicalSwitchPort{ExternalIds: map[string]string{"foo": "baz"}},
+			err:   true,
+		},
+	}
+	for _, tt := range test {
+		t.Run(fmt.Sprintf("Equality Conditional: %s", tt.name), func(t *testing.T) {
+			cond, err := newEqualityConditional(cache.orm, "Logical_Switch_Port", tt.model)
+			assert.Nil(t, err)
+			for model, shouldMatch := range tt.matches {
+				matches, err := cond.Matches(model)
+				if tt.err {
+					assert.NotNil(t, err)
+				} else {
+					assert.Nil(t, err)
+					assert.Equalf(t, shouldMatch, matches, fmt.Sprintf("Match on model %#+v should be %v", model, shouldMatch))
+				}
+			}
+			generated, err := cond.Generate()
+			if tt.err {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.ElementsMatch(t, tt.condition, generated)
+			}
+		})
+	}
+}
+
+func TestPredicateConditional(t *testing.T) {
+	cache := apiTestCache(t)
+	lspcacheList := []Model{
+		&testLogicalSwitchPort{
+			UUID:        aUUID0,
+			Name:        "lsp0",
+			ExternalIds: map[string]string{"foo": "bar"},
+			Enabled:     []bool{true},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID1,
+			Name:        "lsp1",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     []bool{false},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID2,
+			Name:        "lsp2",
+			ExternalIds: map[string]string{"unique": "id"},
+			Enabled:     []bool{false},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID3,
+			Name:        "lsp3",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     []bool{true},
+		},
+	}
+	lspcache := map[string]Model{}
+	for i := range lspcacheList {
+		lspcache[lspcacheList[i].(*testLogicalSwitchPort).UUID] = lspcacheList[i]
+	}
+	cache.cache["Logical_Switch_Port"] = &RowCache{cache: lspcache}
+
+	test := []struct {
+		name      string
+		predicate interface{}
+		condition []ovsdb.Condition
+		matches   map[Model]bool
+		err       bool
+	}{
+		{
+			name: "simple value comparison",
+			predicate: func(lsp *testLogicalSwitchPort) bool {
+				return lsp.UUID == aUUID0
+			},
+			condition: []ovsdb.Condition{
+				{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID0},
+				}},
+			matches: map[Model]bool{
+				&testLogicalSwitchPort{UUID: aUUID0}:              true,
+				&testLogicalSwitchPort{UUID: aUUID1}:              false,
+				&testLogicalSwitchPort{UUID: aUUID0, Name: "foo"}: true,
+			},
+		},
+		{
+			name: "by random field",
+			predicate: func(lsp *testLogicalSwitchPort) bool {
+				return lsp.Enabled[0] == false
+			},
+			condition: []ovsdb.Condition{
+				{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID1},
+				},
+				{
+					Column:   "_uuid",
+					Function: ovsdb.ConditionEqual,
+					Value:    ovsdb.UUID{GoUUID: aUUID2},
+				}},
+			matches: map[Model]bool{
+				&testLogicalSwitchPort{UUID: aUUID1, Enabled: []bool{true}}:  false,
+				&testLogicalSwitchPort{UUID: aUUID1, Enabled: []bool{false}}: true,
+			},
+		},
+	}
+	for _, tt := range test {
+		t.Run(fmt.Sprintf("Predicate Conditional: %s", tt.name), func(t *testing.T) {
+			cond, err := newPredicateConditional("Logical_Switch_Port", cache, tt.predicate)
+			assert.Nil(t, err)
+			for model, shouldMatch := range tt.matches {
+				matches, err := cond.Matches(model)
+				if tt.err {
+					assert.NotNil(t, err)
+				} else {
+					assert.Nil(t, err)
+					assert.Equalf(t, shouldMatch, matches, fmt.Sprintf("Match on model %#+v should be %v", model, shouldMatch))
+				}
+			}
+			generated, err := cond.Generate()
+			if tt.err {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.ElementsMatch(t, tt.condition, generated)
+			}
+		})
+	}
+}
+
+func TestExplicitConditional(t *testing.T) {
+	cache := apiTestCache(t)
+	lspcacheList := []Model{
+		&testLogicalSwitchPort{
+			UUID:        aUUID0,
+			Name:        "lsp0",
+			ExternalIds: map[string]string{"foo": "bar"},
+			Enabled:     []bool{true},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID1,
+			Name:        "lsp1",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     []bool{false},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID2,
+			Name:        "lsp2",
+			ExternalIds: map[string]string{"unique": "id"},
+			Enabled:     []bool{false},
+		},
+		&testLogicalSwitchPort{
+			UUID:        aUUID3,
+			Name:        "lsp3",
+			ExternalIds: map[string]string{"foo": "baz"},
+			Enabled:     []bool{true},
+		},
+	}
+	lspcache := map[string]Model{}
+	for i := range lspcacheList {
+		lspcache[lspcacheList[i].(*testLogicalSwitchPort).UUID] = lspcacheList[i]
+	}
+	cache.cache["Logical_Switch_Port"] = &RowCache{cache: lspcache}
+
+	testObj := &testLogicalSwitchPort{}
+
+	test := []struct {
+		name   string
+		args   []Condition
+		result []ovsdb.Condition
+		err    bool
+	}{
+		{
+			name: "inequality comparison",
+			args: []Condition{
+				{
+					Field:    &testObj.Name,
+					Function: ovsdb.ConditionNotEqual,
+					Value:    "lsp0",
+				},
+			},
+			result: []ovsdb.Condition{
+				{
+					Column:   "name",
+					Function: ovsdb.ConditionNotEqual,
+					Value:    "lsp0",
+				}},
+		},
+		{
+			name: "map comparison",
+			args: []Condition{
+				{
+					Field:    &testObj.ExternalIds,
+					Function: ovsdb.ConditionIncludes,
+					Value:    map[string]string{"foo": "baz"},
+				},
+			},
+			result: []ovsdb.Condition{
+				{
+					Column:   "external_ids",
+					Function: ovsdb.ConditionIncludes,
+					Value:    testOvsMap(t, map[string]string{"foo": "baz"}),
+				}},
+		},
+		{
+			name: "set comparison",
+			args: []Condition{
+				{
+					Field:    &testObj.Enabled,
+					Function: ovsdb.ConditionEqual,
+					Value:    []bool{true},
+				},
+			},
+			result: []ovsdb.Condition{
+				{
+					Column:   "enabled",
+					Function: ovsdb.ConditionEqual,
+					Value:    testOvsSet(t, []bool{true}),
+				}},
+		},
+		{
+			name: "multiple conditions",
+			args: []Condition{
+				{
+					Field:    &testObj.Enabled,
+					Function: ovsdb.ConditionEqual,
+					Value:    []bool{true},
+				},
+				{
+					Field:    &testObj.Name,
+					Function: ovsdb.ConditionNotEqual,
+					Value:    "foo",
+				},
+			},
+			result: []ovsdb.Condition{
+				{
+					Column:   "enabled",
+					Function: ovsdb.ConditionEqual,
+					Value:    testOvsSet(t, []bool{true}),
+				},
+				{
+					Column:   "name",
+					Function: ovsdb.ConditionNotEqual,
+					Value:    "foo",
+				}},
+		},
+	}
+	for _, tt := range test {
+		t.Run(fmt.Sprintf("Explicit Conditional: %s", tt.name), func(t *testing.T) {
+			cond, err := newExplicitConditional(cache.orm, "Logical_Switch_Port", testObj, tt.args...)
+			assert.Nil(t, err)
+			_, err = cond.Matches(testObj)
+			assert.NotNilf(t, err, "Explicit conditions should fail to match on cache")
+			generated, err := cond.Generate()
+			if tt.err {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.ElementsMatch(t, tt.result, generated)
+			}
+		})
+	}
+}
diff --git a/client/orm_test.go b/client/orm_test.go
index 7abf5f7..ee2c18e 100644
--- a/client/orm_test.go
+++ b/client/orm_test.go
@@ -580,9 +580,9 @@ func TestORMCondition(t *testing.T) {
 		},
 	}
 	for _, tt := range tests {
-		t.Run(fmt.Sprintf("newCondition_%s", tt.name), func(t *testing.T) {
+		t.Run(fmt.Sprintf("newEqualityCondition_%s", tt.name), func(t *testing.T) {
 			tt.prepare(&testObj)
-			conds, err := orm.newCondition("TestTable", &testObj, tt.index...)
+			conds, err := orm.newEqualityCondition("TestTable", &testObj, tt.index...)
 			if tt.err {
 				if err == nil {
 					t.Errorf("expected an error but got none")
diff --git a/cmd/modelgen/table_test.go b/cmd/modelgen/table_test.go
index cdcd7cb..1483f31 100644
--- a/cmd/modelgen/table_test.go
+++ b/cmd/modelgen/table_test.go
@@ -66,18 +66,9 @@ type test struct {
 }
 
 func TestFieldName(t *testing.T) {
-	cases := []struct {
-		in       string
-		expected string
-	}{
-		{"foo", "Foo"},
-	}
-	for _, tt := range cases {
-		if s := FieldName(tt.in); s != tt.expected {
-			t.Fatalf("got %s, wanted %s", s, tt.expected)
-		}
+	if s := FieldName("foo"); s != "Foo" {
+		t.Fatalf("got %s, wanted Foo", s)
 	}
-
 }
 
 func TestStructName(t *testing.T) {
@@ -135,23 +126,7 @@ func TestFileName(t *testing.T) {
 }
 
 func TestCamelCase(t *testing.T) {
-	cases := []struct {
-		in       string
-		expected string
-	}{
-		{"foo_bar_baz", "FooBarBaz"},
-		{"foo-bar-baz", "FooBarBaz"},
-		{"foos-bars-bazs", "FoosBarsBazs"},
-		{"ip_port_mappings", "IPPortMappings"},
-		{"external_ids", "ExternalIDs"},
-		{"ip_prefix", "IPPrefix"},
-		{"dns_records", "DNSRecords"},
-		{"logical_ip", "LogicalIP"},
-		{"ip", "IP"},
-	}
-	for _, tt := range cases {
-		if s := camelCase(tt.in); s != tt.expected {
-			t.Fatalf("got %s, wanted %s", s, tt.expected)
-		}
+	if s := camelCase("foo_bar_baz"); s != "FooBarBaz" {
+		t.Fatalf("got %s, wanted FooBarBaz", s)
 	}
 }
diff --git a/ovsdb/bindings_test.go b/ovsdb/bindings_test.go
index 71cd2eb..a0d65ad 100644
--- a/ovsdb/bindings_test.go
+++ b/ovsdb/bindings_test.go
@@ -939,3 +939,129 @@ func TestMutationValidation(t *testing.T) {
 		})
 	}
 }
+
+func TestConditionValidation(t *testing.T) {
+	type Test struct {
+		name      string
+		column    []byte
+		functions []ConditionFunction
+		value     interface{}
+		valid     bool
+	}
+	tests := []Test{
+		{
+			name:      "string",
+			column:    []byte(`{"type":"string"}`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     "foo",
+			valid:     true,
+		},
+		{
+			name:      "uuid",
+			column:    []byte(`{"type":"uuid"}`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     "foo",
+			valid:     true,
+		},
+		{
+			name:      "string wrong type",
+			column:    []byte(`{"type":"string"}`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     42,
+			valid:     false,
+		},
+		{
+			name:      "numeric",
+			column:    []byte(`{"type":"integer"}`),
+			functions: []ConditionFunction{ConditionGreaterThanOrEqual, ConditionGreaterThan, ConditionLessThan, ConditionLessThanOrEqual, ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     1000,
+			valid:     true,
+		},
+		{
+			name:      "numeric wrong type",
+			column:    []byte(`{"type":"integer"}`),
+			functions: []ConditionFunction{ConditionGreaterThanOrEqual, ConditionGreaterThan, ConditionLessThan, ConditionLessThanOrEqual, ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     "foo",
+			valid:     false,
+		},
+		{
+			name: "set",
+			column: []byte(`{
+				   "type": {
+				     "key": "string",
+				     "max": "unlimited",
+				     "min": 0
+				   }
+				 }`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     []string{"foo", "bar"},
+			valid:     true,
+		},
+		{
+			name: "set wrong type",
+			column: []byte(`{
+				   "type": {
+				     "key": "string",
+				     "max": "unlimited",
+				     "min": 0
+				   }
+				 }`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     32,
+			valid:     false,
+		},
+		{
+			name: "set wrong type2",
+			column: []byte(`{
+				   "type": {
+				     "key": "string",
+				     "max": "unlimited",
+				     "min": 0
+				   }
+				 }`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     "foo",
+			valid:     false,
+		},
+		{
+			name: "map",
+			column: []byte(`{
+				   "type": {
+				     "key": "string",
+				     "value": "string"
+				   }
+				 }`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     map[string]string{"foo": "bar"},
+			valid:     true,
+		},
+		{
+			name: "map wrong type",
+			column: []byte(`{
+				   "type": {
+				     "key": "string",
+				     "value": "string"
+				   }
+				 }`),
+			functions: []ConditionFunction{ConditionEqual, ConditionIncludes, ConditionNotEqual, ConditionExcludes},
+			value:     map[string]int{"foo": 42},
+			valid:     false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(fmt.Sprintf("ConditionValidation: %s", test.name), func(t *testing.T) {
+			var column ColumnSchema
+			err := json.Unmarshal(test.column, &column)
+			assert.Nil(t, err)
+
+			for _, f := range test.functions {
+				result := ValidateCondition(&column, f, test.value)
+				if test.valid {
+					assert.Nil(t, result)
+				} else {
+					assert.NotNil(t, result)
+				}
+			}
+		})
+	}
+}