Repository: ovn-org/libovsdb
Pull request #375 (2024-01-09 15:46:09)

Implement test server referential integrity 
Implement referential integrity and garbage collection as described in
RFC7047.

This is achieved keeping track of inverse references. A reference
tracker is used in the transaction to keep these references updated, to
check for referential integrity related violations and to add additional
updates to the transaction resulting from reference garbage collection.
The inverse references are stored in the database on commit.
The updates resulting from reference garbage collection are also sent to
any monitoring clients as expected.

The database package was split so that new types could be introduced 
without incurring in circular dependencies.

Fixed a couple of issues found along the way.

As it usually happens, a good portion of the code is just testing.

This should only have code changes impacting the test server, no impacts 
to the client.

Fixes: https://github.com/ovn-org/libovsdb/issues/219
Partially Fixes: https://github.com/ovn-org/libovsdb/issues/338


Patch:
diff --git a/README.md b/README.md
index f43869a..f35e861 100644
--- a/README.md
+++ b/README.md
@@ -131,7 +131,7 @@ This package is divided into several sub-packages. Documentation for each sub-pa
 * **cache**: model-based cache [![godoc for libovsdb/cache][cachebadge]][cachedoc]
 * **modelgen**: common code-generator functions  [![godoc for libovsdb/modelgen][genbadge]][gendoc]
 * **server**: ovsdb test server [![godoc for libovsdb/server][serverbadge]][serverdoc]
-* **database**: in-memory database for the server [![godoc for libovsdb/database][dbbadge]][dbdoc]
+* **database**: database related types, interfaces and implementations [![godoc for libovsdb/database][dbbadge]][dbdoc]
 * **updates**: common code to handle model updates [![godoc for libovsdb/updates][updatesbadge]][updatesdoc]
 
 [doc]: https://pkg.go.dev/
diff --git a/client/api.go b/client/api.go
index 9c29563..4977589 100644
--- a/client/api.go
+++ b/client/api.go
@@ -51,9 +51,9 @@ type API interface {
 	Get(context.Context, model.Model) error
 
 	// Create returns the operation needed to add the model(s) to the Database
-	// Only fields with non-default values will be added to the transaction
-	// If the field associated with column "_uuid" has some content, it will be
-	// treated as named-uuid
+	// Only fields with non-default values will be added to the transaction. If
+	// the field associated with column "_uuid" has some content other than a
+	// UUID, it will be treated as named-uuid
 	Create(...model.Model) ([]ovsdb.Operation, error)
 }
 
diff --git a/database/database.go b/database/database.go
index 47d1450..12f1222 100644
--- a/database/database.go
+++ b/database/database.go
@@ -1,122 +1,33 @@
 package database
 
 import (
-	"fmt"
-	"sync"
-
 	"github.com/google/uuid"
-	"github.com/ovn-org/libovsdb/cache"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
 )
 
-// Database abstracts database operations from ovsdb
+// Database abstracts a database that a server can use to store and transact data
 type Database interface {
 	CreateDatabase(database string, model ovsdb.DatabaseSchema) error
 	Exists(database string) bool
+	NewTransaction(database string) Transaction
 	Commit(database string, id uuid.UUID, update Update) error
 	CheckIndexes(database string, table string, m model.Model) error
 	List(database, table string, conditions ...ovsdb.Condition) (map[string]model.Model, error)
 	Get(database, table string, uuid string) (model.Model, error)
+	GetReferences(database, table, row string) (References, error)
+}
+
+// Transaction abstracts a database transaction that can generate database
+// updates
+type Transaction interface {
+	Transact(operations ...ovsdb.Operation) ([]*ovsdb.OperationResult, Update)
 }
 
-// Update abstacts a database update in both ovsdb and model notation
+// Update abstracts an update that can be committed to a database
 type Update interface {
 	GetUpdatedTables() []string
 	ForEachModelUpdate(table string, do func(uuid string, old, new model.Model) error) error
 	ForEachRowUpdate(table string, do func(uuid string, row ovsdb.RowUpdate2) error) error
-}
-
-type inMemoryDatabase struct {
-	databases map[string]*cache.TableCache
-	models    map[string]model.ClientDBModel
-	mutex     sync.RWMutex
-}
-
-func NewInMemoryDatabase(models map[string]model.ClientDBModel) Database {
-	return &inMemoryDatabase{
-		databases: make(map[string]*cache.TableCache),
-		models:    models,
-		mutex:     sync.RWMutex{},
-	}
-}
-
-func (db *inMemoryDatabase) CreateDatabase(name string, schema ovsdb.DatabaseSchema) error {
-	db.mutex.Lock()
-	defer db.mutex.Unlock()
-	var mo model.ClientDBModel
-	var ok bool
-	if mo, ok = db.models[schema.Name]; !ok {
-		return fmt.Errorf("no db model provided for schema with name %s", name)
-	}
-	dbModel, errs := model.NewDatabaseModel(schema, mo)
-	if len(errs) > 0 {
-		return fmt.Errorf("failed to create DatabaseModel: %#+v", errs)
-	}
-	database, err := cache.NewTableCache(dbModel, nil, nil)
-	if err != nil {
-		return err
-	}
-	db.databases[name] = database
-	return nil
-}
-
-func (db *inMemoryDatabase) Exists(name string) bool {
-	db.mutex.RLock()
-	defer db.mutex.RUnlock()
-	_, ok := db.databases[name]
-	return ok
-}
-
-func (db *inMemoryDatabase) Commit(database string, id uuid.UUID, update Update) error {
-	if !db.Exists(database) {
-		return fmt.Errorf("db does not exist")
-	}
-	db.mutex.RLock()
-	targetDb := db.databases[database]
-	db.mutex.RUnlock()
-
-	return targetDb.ApplyCacheUpdate(update)
-}
-
-func (db *inMemoryDatabase) CheckIndexes(database string, table string, m model.Model) error {
-	if !db.Exists(database) {
-		return nil
-	}
-	db.mutex.RLock()
-	targetDb := db.databases[database]
-	db.mutex.RUnlock()
-	targetTable := targetDb.Table(table)
-	return targetTable.IndexExists(m)
-}
-
-func (db *inMemoryDatabase) List(database, table string, conditions ...ovsdb.Condition) (map[string]model.Model, error) {
-	if !db.Exists(database) {
-		return nil, fmt.Errorf("db does not exist")
-	}
-	db.mutex.RLock()
-	targetDb := db.databases[database]
-	db.mutex.RUnlock()
-
-	targetTable := targetDb.Table(table)
-	if targetTable == nil {
-		return nil, fmt.Errorf("table does not exist")
-	}
-
-	return targetTable.RowsByCondition(conditions)
-}
-
-func (db *inMemoryDatabase) Get(database, table string, uuid string) (model.Model, error) {
-	if !db.Exists(database) {
-		return nil, fmt.Errorf("db does not exist")
-	}
-	db.mutex.RLock()
-	targetDb := db.databases[database]
-	db.mutex.RUnlock()
-
-	targetTable := targetDb.Table(table)
-	if targetTable == nil {
-		return nil, fmt.Errorf("table does not exist")
-	}
-	return targetTable.Row(uuid), nil
+	ForReferenceUpdates(do func(references References) error) error
 }
diff --git a/database/doc.go b/database/doc.go
index 8d3bdcb..c0a858c 100644
--- a/database/doc.go
+++ b/database/doc.go
@@ -1,4 +1,5 @@
 /*
-Package database provides an in-memory database implementation.
+Package database collects database related types, interfaces and
+implementations.
 */
 package database
diff --git a/database/inmemory/doc.go b/database/inmemory/doc.go
new file mode 100644
index 0000000..bde3ffc
--- /dev/null
+++ b/database/inmemory/doc.go
@@ -0,0 +1,4 @@
+/*
+Package inmemory provides a in-memory database implementation
+*/
+package inmemory
diff --git a/database/inmemory/inmemory.go b/database/inmemory/inmemory.go
new file mode 100644
index 0000000..6c1dce9
--- /dev/null
+++ b/database/inmemory/inmemory.go
@@ -0,0 +1,145 @@
+package inmemory
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"sync"
+
+	"github.com/go-logr/logr"
+	"github.com/go-logr/stdr"
+	"github.com/google/uuid"
+	"github.com/ovn-org/libovsdb/cache"
+	dbase "github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/database/transaction"
+	"github.com/ovn-org/libovsdb/model"
+	"github.com/ovn-org/libovsdb/ovsdb"
+)
+
+type inMemoryDatabase struct {
+	databases  map[string]*cache.TableCache
+	models     map[string]model.ClientDBModel
+	references map[string]dbase.References
+	logger     *logr.Logger
+	mutex      sync.RWMutex
+}
+
+func NewDatabase(models map[string]model.ClientDBModel) dbase.Database {
+	logger := stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags), stdr.Options{LogCaller: stdr.All}).WithName("database")
+	return &inMemoryDatabase{
+		databases:  make(map[string]*cache.TableCache),
+		models:     models,
+		references: make(map[string]dbase.References),
+		mutex:      sync.RWMutex{},
+		logger:     &logger,
+	}
+}
+
+func (db *inMemoryDatabase) NewTransaction(dbName string) dbase.Transaction {
+	db.mutex.Lock()
+	defer db.mutex.Unlock()
+	var model model.DatabaseModel
+	if database, ok := db.databases[dbName]; ok {
+		model = database.DatabaseModel()
+	}
+	transaction := transaction.NewTransaction(model, dbName, db, db.logger)
+	return &transaction
+}
+
+func (db *inMemoryDatabase) CreateDatabase(name string, schema ovsdb.DatabaseSchema) error {
+	db.mutex.Lock()
+	defer db.mutex.Unlock()
+	var mo model.ClientDBModel
+	var ok bool
+	if mo, ok = db.models[schema.Name]; !ok {
+		return fmt.Errorf("no db model provided for schema with name %s", name)
+	}
+	dbModel, errs := model.NewDatabaseModel(schema, mo)
+	if len(errs) > 0 {
+		return fmt.Errorf("failed to create DatabaseModel: %#+v", errs)
+	}
+	database, err := cache.NewTableCache(dbModel, nil, nil)
+	if err != nil {
+		return err
+	}
+	db.databases[name] = database
+	db.references[name] = make(dbase.References)
+	return nil
+}
+
+func (db *inMemoryDatabase) Exists(name string) bool {
+	db.mutex.RLock()
+	defer db.mutex.RUnlock()
+	_, ok := db.databases[name]
+	return ok
+}
+
+func (db *inMemoryDatabase) Commit(database string, id uuid.UUID, update dbase.Update) error {
+	if !db.Exists(database) {
+		return fmt.Errorf("db does not exist")
+	}
+	db.mutex.RLock()
+	targetDb := db.databases[database]
+	db.mutex.RUnlock()
+
+	err := targetDb.ApplyCacheUpdate(update)
+	if err != nil {
+		return err
+	}
+
+	return update.ForReferenceUpdates(func(references dbase.References) error {
+		db.references[database].UpdateReferences(references)
+		return nil
+	})
+}
+
+func (db *inMemoryDatabase) CheckIndexes(database string, table string, m model.Model) error {
+	if !db.Exists(database) {
+		return nil
+	}
+	db.mutex.RLock()
+	targetDb := db.databases[database]
+	db.mutex.RUnlock()
+	targetTable := targetDb.Table(table)
+	return targetTable.IndexExists(m)
+}
+
+func (db *inMemoryDatabase) List(database, table string, conditions ...ovsdb.Condition) (map[string]model.Model, error) {
+	if !db.Exists(database) {
+		return nil, fmt.Errorf("db does not exist")
+	}
+	db.mutex.RLock()
+	targetDb := db.databases[database]
+	db.mutex.RUnlock()
+
+	targetTable := targetDb.Table(table)
+	if targetTable == nil {
+		return nil, fmt.Errorf("table does not exist")
+	}
+
+	return targetTable.RowsByCondition(conditions)
+}
+
+func (db *inMemoryDatabase) Get(database, table string, uuid string) (model.Model, error) {
+	if !db.Exists(database) {
+		return nil, fmt.Errorf("db does not exist")
+	}
+	db.mutex.RLock()
+	targetDb := db.databases[database]
+	db.mutex.RUnlock()
+
+	targetTable := targetDb.Table(table)
+	if targetTable == nil {
+		return nil, fmt.Errorf("table does not exist")
+	}
+	return targetTable.Row(uuid), nil
+}
+
+func (db *inMemoryDatabase) GetReferences(database, table, row string) (dbase.References, error) {
+	if !db.Exists(database) {
+		return nil, fmt.Errorf("db does not exist")
+	}
+	db.mutex.RLock()
+	defer db.mutex.RUnlock()
+	return db.references[database].GetReferences(table, row), nil
+}
diff --git a/database/references.go b/database/references.go
new file mode 100644
index 0000000..d8181a7
--- /dev/null
+++ b/database/references.go
@@ -0,0 +1,71 @@
+package database
+
+// References tracks the references to rows from other rows at specific
+// locations in the schema.
+type References map[ReferenceSpec]Reference
+
+// ReferenceSpec specifies details about where in the schema a reference occurs.
+type ReferenceSpec struct {
+	// ToTable is the table of the row to which the reference is made
+	ToTable string
+
+	// FromTable is the table of the row from which the reference is made
+	FromTable string
+
+	// FromColumn is the column of the row from which the reference is made
+	FromColumn string
+
+	// FromValue flags if the reference is made on a map key or map value when
+	// the column is a map
+	FromValue bool
+}
+
+// Reference maps the UUIDs of rows to which the reference is made to the
+// rows it is made from
+type Reference map[string][]string
+
+// GetReferences gets references to a row
+func (rs References) GetReferences(table, uuid string) References {
+	refs := References{}
+	for spec, values := range rs {
+		if spec.ToTable != table {
+			continue
+		}
+		if _, ok := values[uuid]; ok {
+			refs[spec] = Reference{uuid: values[uuid]}
+		}
+	}
+	return refs
+}
+
+// UpdateReferences updates the references with the provided ones. Dangling
+// references, that is, the references of rows that are no longer referenced
+// from anywhere, are cleaned up.
+func (rs References) UpdateReferences(other References) {
+	for spec, otherRefs := range other {
+		for to, from := range otherRefs {
+			rs.updateReference(spec, to, from)
+		}
+	}
+}
+
+// updateReference updates the references to a row at a specific location in the
+// schema
+func (rs References) updateReference(spec ReferenceSpec, to string, from []string) {
+	thisRefs, ok := rs[spec]
+	if !ok && len(from) > 0 {
+		// add references from a previously untracked location
+		rs[spec] = Reference{to: from}
+		return
+	}
+	if len(from) > 0 {
+		// replace references to this row at this specific location
+		thisRefs[to] = from
+		return
+	}
+	// otherwise remove previously tracked references
+	delete(thisRefs, to)
+	if len(thisRefs) == 0 {
+		delete(rs, spec)
+	}
+}
diff --git a/database/transaction/doc.go b/database/transaction/doc.go
new file mode 100644
index 0000000..36d35aa
--- /dev/null
+++ b/database/transaction/doc.go
@@ -0,0 +1,4 @@
+/*
+Package transaction provides a transaction implementation
+*/
+package transaction
diff --git a/database/errors.go b/database/transaction/errors.go
similarity index 95%
rename from database/errors.go
rename to database/transaction/errors.go
index 979752c..35e47c7 100644
--- a/database/errors.go
+++ b/database/transaction/errors.go
@@ -1,4 +1,4 @@
-package database
+package transaction
 
 import (
 	"fmt"
diff --git a/database/transaction.go b/database/transaction/transaction.go
similarity index 78%
rename from database/transaction.go
rename to database/transaction/transaction.go
index 97d586a..69736d0 100644
--- a/database/transaction.go
+++ b/database/transaction/transaction.go
@@ -1,4 +1,4 @@
-package database
+package transaction
 
 import (
 	"fmt"
@@ -8,6 +8,7 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/google/uuid"
 	"github.com/ovn-org/libovsdb/cache"
+	"github.com/ovn-org/libovsdb/database"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
 	"github.com/ovn-org/libovsdb/updates"
@@ -19,32 +20,43 @@ type Transaction struct {
 	DeletedRows map[string]struct{}
 	Model       model.DatabaseModel
 	DbName      string
-	Database    Database
+	Database    database.Database
+	logger      *logr.Logger
 }
 
-func NewTransaction(model model.DatabaseModel, dbName string, database Database, logger *logr.Logger) Transaction {
+func NewTransaction(model model.DatabaseModel, dbName string, database database.Database, logger *logr.Logger) Transaction {
 	if logger != nil {
 		l := logger.WithName("transaction")
 		logger = &l
 	}
-	cache, err := cache.NewTableCache(model, nil, logger)
-	if err != nil {
-		panic(err)
-	}
+
 	return Transaction{
 		ID:          uuid.New(),
-		Cache:       cache,
 		DeletedRows: make(map[string]struct{}),
 		Model:       model,
 		DbName:      dbName,
 		Database:    database,
+		logger:      logger,
 	}
 }
 
-func (t *Transaction) Transact(operations []ovsdb.Operation) ([]*ovsdb.OperationResult, Update) {
-	results := []*ovsdb.OperationResult{}
+func (t *Transaction) Transact(operations ...ovsdb.Operation) ([]*ovsdb.OperationResult, database.Update) {
+	results := make([]*ovsdb.OperationResult, len(operations), len(operations)+1)
 	update := updates.ModelUpdates{}
 
+	if !t.Database.Exists(t.DbName) {
+		r := ovsdb.ResultFromError(fmt.Errorf("database does not exist"))
+		results[0] = &r
+		return results, updates.NewDatabaseUpdate(update, nil)
+	}
+
+	err := t.initializeCache()
+	if err != nil {
+		r := ovsdb.ResultFromError(err)
+		results[0] = &r
+		return results, updates.NewDatabaseUpdate(update, nil)
+	}
+
 	// Every Insert operation must have a UUID
 	for i := range operations {
 		op := &operations[i]
@@ -54,31 +66,15 @@ func (t *Transaction) Transact(operations []ovsdb.Operation) ([]*ovsdb.Operation
 	}
 
 	// Ensure Named UUIDs are expanded in all operations
-	var err error
 	operations, err = ovsdb.ExpandNamedUUIDs(operations, &t.Model.Schema)
 	if err != nil {
 		r := ovsdb.ResultFromError(err)
-		return []*ovsdb.OperationResult{&r}, nil
+		results[0] = &r
+		return results, updates.NewDatabaseUpdate(update, nil)
 	}
 
 	var r ovsdb.OperationResult
-	for _, op := range operations {
-		// if we had a previous error, just append a nil result for every op
-		// after that
-		if r.Error != "" {
-			results = append(results, nil)
-			continue
-		}
-
-		// simple case: database name does not exist
-		if !t.Database.Exists(t.DbName) {
-			r = ovsdb.OperationResult{
-				Error: "database does not exist",
-			}
-			results = append(results, &r)
-			continue
-		}
-
+	for i, op := range operations {
 		var u *updates.ModelUpdates
 		switch op.Op {
 		case ovsdb.OperationInsert:
@@ -118,12 +114,39 @@ func (t *Transaction) Transact(operations []ovsdb.Operation) ([]*ovsdb.Operation
 		}
 
 		result := r
-		results = append(results, &result)
+		results[i] = &result
+
+		// if an operation failed, no need to process any further operation
+		if r.Error != "" {
+			break
+		}
 	}
 
 	// if an operation failed, no need to do any further validation
 	if r.Error != "" {
-		return results, update
+		return results, updates.NewDatabaseUpdate(update, nil)
+	}
+
+	// if there is no updates, no need to do any further validation
+	if len(update.GetUpdatedTables()) == 0 {
+		return results, updates.NewDatabaseUpdate(update, nil)
+	}
+
+	// check & update references
+	update, refUpdates, refs, err := updates.ProcessReferences(t.Model, t.Database, update)
+	if err != nil {
+		r = ovsdb.ResultFromError(err)
+		results = append(results, &r)
+		return results, updates.NewDatabaseUpdate(update, refs)
+	}
+
+	// apply updates resulting from referential integrity to the transaction
+	// caches so they are accounted for when checking index constraints
+	err = t.applyReferenceUpdates(refUpdates)
+	if err != nil {
+		r = ovsdb.ResultFromError(err)
+		results = append(results, &r)
+		return results, updates.NewDatabaseUpdate(update, refs)
 	}
 
 	// check index constraints
@@ -136,12 +159,58 @@ func (t *Transaction) Transact(operations []ovsdb.Operation) ([]*ovsdb.Operation
 			r := ovsdb.ResultFromError(err)
 			results = append(results, &r)
 		}
+
+		return results, updates.NewDatabaseUpdate(update, refs)
+	}
+
+	return results, updates.NewDatabaseUpdate(update, refs)
+}
+
+func (t *Transaction) applyReferenceUpdates(update updates.ModelUpdates) error {
+	tables := update.GetUpdatedTables()
+	for _, table := range tables {
+		err := update.ForEachModelUpdate(table, func(uuid string, old, new model.Model) error {
+			// track deleted rows due to reference updates
+			if old != nil && new == nil {
+				t.DeletedRows[uuid] = struct{}{}
+			}
+			// warm the cache with updated and deleted rows due to reference
+			// updates
+			if old != nil && !t.Cache.Table(table).HasRow(uuid) {
+				row, err := t.Database.Get(t.DbName, table, uuid)
+				if err != nil {
+					return err
+				}
+				err = t.Cache.Table(table).Create(uuid, row, false)
+				if err != nil {
+					return err
+				}
+			}
+			return nil
+		})
+		if err != nil {
+			return err
+		}
 	}
+	// apply reference updates to the cache
+	return t.Cache.ApplyCacheUpdate(update)
+}
 
-	return results, update
+func (t *Transaction) initializeCache() error {
+	if t.Cache != nil {
+		return nil
+	}
+	var err error
+	t.Cache, err = cache.NewTableCache(t.Model, nil, t.logger)
+	return err
 }
 
 func (t *Transaction) rowsFromTransactionCacheAndDatabase(table string, where []ovsdb.Condition) (map[string]model.Model, error) {
+	err := t.initializeCache()
+	if err != nil {
+		return nil, err
+	}
+
 	txnRows, err := t.Cache.Table(table).RowsByCondition(where)
 	if err != nil {
 		return nil, fmt.Errorf("failed getting rows for table %s from transaction cache: %v", table, err)
diff --git a/example/ovsdb-server/main.go b/example/ovsdb-server/main.go
index e958280..5798e7d 100644
--- a/example/ovsdb-server/main.go
+++ b/example/ovsdb-server/main.go
@@ -13,7 +13,7 @@ import (
 	"time"
 
 	"github.com/ovn-org/libovsdb/client"
-	"github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/database/inmemory"
 	"github.com/ovn-org/libovsdb/example/vswitchd"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
@@ -58,7 +58,7 @@ func main() {
 		log.Fatal(err)
 	}
 
-	ovsDB := database.NewInMemoryDatabase(map[string]model.ClientDBModel{
+	ovsDB := inmemory.NewDatabase(map[string]model.ClientDBModel{
 		schema.Name: clientDBModel,
 	})
 
diff --git a/ovsdb/error.go b/ovsdb/error.go
index 0803894..4a85c54 100644
--- a/ovsdb/error.go
+++ b/ovsdb/error.go
@@ -128,6 +128,10 @@ type ReferentialIntegrityViolation struct {
 	operation *Operation
 }
 
+func NewReferentialIntegrityViolation(details string) *ReferentialIntegrityViolation {
+	return &ReferentialIntegrityViolation{details: details}
+}
+
 // Error implements the error interface
 func (e *ReferentialIntegrityViolation) Error() string {
 	msg := referentialIntegrityViolation
diff --git a/ovsdb/schema.go b/ovsdb/schema.go
index cf80aa5..285d1e0 100644
--- a/ovsdb/schema.go
+++ b/ovsdb/schema.go
@@ -12,9 +12,10 @@ import (
 
 // DatabaseSchema is a database schema according to RFC7047
 type DatabaseSchema struct {
-	Name    string                 `json:"name"`
-	Version string                 `json:"version"`
-	Tables  map[string]TableSchema `json:"tables"`
+	Name          string                 `json:"name"`
+	Version       string                 `json:"version"`
+	Tables        map[string]TableSchema `json:"tables"`
+	allTablesRoot *bool
 }
 
 // UUIDColumn is a static column that represents the _uuid column, common to all tables
@@ -30,6 +31,32 @@ func (schema DatabaseSchema) Table(tableName string) *TableSchema {
 	return nil
 }
 
+// IsRoot whether a table is root or not
+func (schema DatabaseSchema) IsRoot(tableName string) (bool, error) {
+	t := schema.Table(tableName)
+	if t == nil {
+		return false, fmt.Errorf("Table %s not in schame", tableName)
+	}
+	if t.IsRoot {
+		return true, nil
+	}
+	// As per RFC7047, for compatibility with schemas created before
+	// "isRoot" was introduced, if "isRoot" is omitted or false in every
+	// <table-schema> in a given <database-schema>, then every table is part
+	// of the root set.
+	if schema.allTablesRoot == nil {
+		allTablesRoot := true
+		for _, tSchema := range schema.Tables {
+			if tSchema.IsRoot {
+				allTablesRoot = false
+				break
+			}
+		}
+		schema.allTablesRoot = &allTablesRoot
+	}
+	return *schema.allTablesRoot, nil
+}
+
 // Print will print the contents of the DatabaseSchema
 func (schema DatabaseSchema) Print(w io.Writer) {
 	fmt.Fprintf(w, "%s, (%s)\n", schema.Name, schema.Version)
@@ -104,6 +131,7 @@ func (schema DatabaseSchema) ValidateOperations(operations ...Operation) bool {
 type TableSchema struct {
 	Columns map[string]*ColumnSchema `json:"columns"`
 	Indexes [][]string               `json:"indexes,omitempty"`
+	IsRoot  bool                     `json:"isRoot,omitempty"`
 }
 
 // Column returns the Column object for a specific column name
@@ -124,7 +152,7 @@ of this library, we define an ExtendedType that includes all possible column typ
 atomic fields).
 */
 
-//ExtendedType includes atomic types as defined in the RFC plus Enum, Map and Set
+// ExtendedType includes atomic types as defined in the RFC plus Enum, Map and Set
 type ExtendedType = string
 
 // RefType is used to define the possible RefTypes
diff --git a/ovsdb/serverdb/model.go b/ovsdb/serverdb/model.go
index c8e5cec..3c117fa 100644
--- a/ovsdb/serverdb/model.go
+++ b/ovsdb/serverdb/model.go
@@ -83,7 +83,8 @@ var schema = `{
             "max": 1
           }
         }
-      }
+      },
+      "isRoot": true
     }
   }
 }`
diff --git a/server/server.go b/server/server.go
index 0100d76..ec60ea5 100644
--- a/server/server.go
+++ b/server/server.go
@@ -218,11 +218,8 @@ func (o *OvsdbServer) Transact(client *rpc2.Client, args []json.RawMessage, repl
 }
 
 func (o *OvsdbServer) transact(name string, operations []ovsdb.Operation) ([]*ovsdb.OperationResult, database.Update) {
-	o.modelsMutex.Lock()
-	dbModel := o.models[name]
-	o.modelsMutex.Unlock()
-	transaction := database.NewTransaction(dbModel, name, o.db, &o.logger)
-	return transaction.Transact(operations)
+	transaction := o.db.NewTransaction(name)
+	return transaction.Transact(operations...)
 }
 
 // Cancel cancels the last transaction
@@ -255,21 +252,20 @@ func (o *OvsdbServer) Monitor(client *rpc2.Client, args []json.RawMessage, reply
 		}
 	}
 
-	o.modelsMutex.Lock()
-	dbModel := o.models[db]
-	o.modelsMutex.Unlock()
-	transaction := database.NewTransaction(dbModel, db, o.db, &o.logger)
+	transaction := o.db.NewTransaction(db)
 
 	tableUpdates := make(ovsdb.TableUpdates)
 	for t, request := range request {
-		rows := transaction.Select(t, nil, request.Columns)
-		if len(rows.Rows) == 0 {
+		op := ovsdb.Operation{Op: ovsdb.OperationSelect, Table: t, Columns: request.Columns}
+		result, _ := transaction.Transact(op)
+		if len(result) == 0 || len(result[0].Rows) == 0 {
 			continue
 		}
-		tableUpdates[t] = make(ovsdb.TableUpdate, len(rows.Rows))
-		for i := range rows.Rows {
-			uuid := rows.Rows[i]["_uuid"].(ovsdb.UUID).GoUUID
-			tableUpdates[t][uuid] = &ovsdb.RowUpdate{New: &rows.Rows[i]}
+		rows := result[0].Rows
+		tableUpdates[t] = make(ovsdb.TableUpdate, len(rows))
+		for i := range rows {
+			uuid := rows[i]["_uuid"].(ovsdb.UUID).GoUUID
+			tableUpdates[t][uuid] = &ovsdb.RowUpdate{New: &rows[i]}
 		}
 	}
 	*reply = tableUpdates
@@ -302,21 +298,20 @@ func (o *OvsdbServer) MonitorCond(client *rpc2.Client, args []json.RawMessage, r
 		}
 	}
 
-	o.modelsMutex.Lock()
-	dbModel := o.models[db]
-	o.modelsMutex.Unlock()
-	transaction := database.NewTransaction(dbModel, db, o.db, &o.logger)
+	transaction := o.db.NewTransaction(db)
 
 	tableUpdates := make(ovsdb.TableUpdates2)
 	for t, request := range request {
-		rows := transaction.Select(t, nil, request.Columns)
-		if len(rows.Rows) == 0 {
+		op := ovsdb.Operation{Op: ovsdb.OperationSelect, Table: t, Columns: request.Columns}
+		result, _ := transaction.Transact(op)
+		if len(result) == 0 || len(result[0].Rows) == 0 {
 			continue
 		}
-		tableUpdates[t] = make(ovsdb.TableUpdate2, len(rows.Rows))
-		for i := range rows.Rows {
-			uuid := rows.Rows[i]["_uuid"].(ovsdb.UUID).GoUUID
-			tableUpdates[t][uuid] = &ovsdb.RowUpdate2{Initial: &rows.Rows[i]}
+		rows := result[0].Rows
+		tableUpdates[t] = make(ovsdb.TableUpdate2, len(rows))
+		for i := range rows {
+			uuid := rows[i]["_uuid"].(ovsdb.UUID).GoUUID
+			tableUpdates[t][uuid] = &ovsdb.RowUpdate2{Initial: &rows[i]}
 		}
 	}
 	*reply = tableUpdates
@@ -349,21 +344,20 @@ func (o *OvsdbServer) MonitorCondSince(client *rpc2.Client, args []json.RawMessa
 		}
 	}
 
-	o.modelsMutex.Lock()
-	dbModel := o.models[db]
-	o.modelsMutex.Unlock()
-	transaction := database.NewTransaction(dbModel, db, o.db, &o.logger)
+	transaction := o.db.NewTransaction(db)
 
 	tableUpdates := make(ovsdb.TableUpdates2)
 	for t, request := range request {
-		rows := transaction.Select(t, nil, request.Columns)
-		if len(rows.Rows) == 0 {
+		op := ovsdb.Operation{Op: ovsdb.OperationSelect, Table: t, Columns: request.Columns}
+		result, _ := transaction.Transact(op)
+		if len(result) == 0 || len(result[0].Rows) == 0 {
 			continue
 		}
-		tableUpdates[t] = make(ovsdb.TableUpdate2, len(rows.Rows))
-		for i := range rows.Rows {
-			uuid := rows.Rows[i]["_uuid"].(ovsdb.UUID).GoUUID
-			tableUpdates[t][uuid] = &ovsdb.RowUpdate2{Initial: &rows.Rows[i]}
+		rows := result[0].Rows
+		tableUpdates[t] = make(ovsdb.TableUpdate2, len(rows))
+		for i := range rows {
+			uuid := rows[i]["_uuid"].(ovsdb.UUID).GoUUID
+			tableUpdates[t][uuid] = &ovsdb.RowUpdate2{Initial: &rows[i]}
 		}
 	}
 	*reply = ovsdb.MonitorCondSinceReply{Found: false, LastTransactionID: "00000000-0000-0000-000000000000", Updates: tableUpdates}
diff --git a/updates/merge.go b/updates/merge.go
index 903490c..562f226 100644
--- a/updates/merge.go
+++ b/updates/merge.go
@@ -132,6 +132,16 @@ func mergeModifyRow(ts *ovsdb.TableSchema, o, a, b *ovsdb.Row) *ovsdb.Row {
 				// assume zero value if original does not have the column
 				o = reflect.Zero(reflect.TypeOf(v)).Interface()
 			}
+			if set, ok := o.(ovsdb.OvsSet); ok {
+				// atomic optional values are cleared out with an empty set
+				// if the original value was also cleared out, use an empty set
+				// instead of a nil set so that mergeAtomicDifference notices
+				// that we are returning to the original value
+				if set.GoSet == nil {
+					set.GoSet = []interface{}{}
+				}
+				o = set
+			}
 			result, changed = mergeAtomicDifference(o, aMod[k], v)
 		}
 
diff --git a/updates/references.go b/updates/references.go
new file mode 100644
index 0000000..938d02a
--- /dev/null
+++ b/updates/references.go
@@ -0,0 +1,797 @@
+package updates
+
+import (
+	"fmt"
+
+	"github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/model"
+	"github.com/ovn-org/libovsdb/ovsdb"
+)
+
+// ReferenceProvider should be implemented by a database that tracks references
+type ReferenceProvider interface {
+	// GetReferences provides the references to the provided row
+	GetReferences(database, table, uuid string) (database.References, error)
+	// Get provides the corresponding model
+	Get(database, table string, uuid string) (model.Model, error)
+}
+
+// DatabaseUpdate bundles updates together with the updated
+// reference information
+type DatabaseUpdate struct {
+	ModelUpdates
+	referenceUpdates database.References
+}
+
+func (u DatabaseUpdate) ForReferenceUpdates(do func(references database.References) error) error {
+	refsCopy := database.References{}
+	// since refsCopy is empty, this will just copy everything
+	applyReferenceModifications(refsCopy, u.referenceUpdates)
+	return do(refsCopy)
+}
+
+func NewDatabaseUpdate(updates ModelUpdates, references database.References) DatabaseUpdate {
+	return DatabaseUpdate{
+		ModelUpdates:     updates,
+		referenceUpdates: references,
+	}
+}
+
+// ProcessReferences tracks referential integrity for the provided set of
+// updates. It returns an updated set of updates which includes additional
+// updates and updated references as a result of the reference garbage
+// collection described in RFC7047. These additional updates resulting from the
+// reference garbage collection are also returned separately. Any constraint or
+// referential integrity violation is returned as an error.
+func ProcessReferences(dbModel model.DatabaseModel, provider ReferenceProvider, updates ModelUpdates) (ModelUpdates, ModelUpdates, database.References, error) {
+	referenceTracker := newReferenceTracker(dbModel, provider)
+	return referenceTracker.processReferences(updates)
+}
+
+type referenceTracker struct {
+	dbModel  model.DatabaseModel
+	provider ReferenceProvider
+
+	// updates that are being processed
+	updates ModelUpdates
+
+	// references are the updated references by the set of updates processed
+	references database.References
+
+	// helper maps to track the rows that we are processing and their tables
+	tracked map[string]string
+	added   map[string]string
+	deleted map[string]string
+}
+
+func newReferenceTracker(dbModel model.DatabaseModel, provider ReferenceProvider) *referenceTracker {
+	return &referenceTracker{
+		dbModel:  dbModel,
+		provider: provider,
+	}
+}
+
+func (rt *referenceTracker) processReferences(updates ModelUpdates) (ModelUpdates, ModelUpdates, database.References, error) {
+	rt.updates = updates
+	rt.tracked = make(map[string]string)
+	rt.added = make(map[string]string)
+	rt.deleted = make(map[string]string)
+	rt.references = make(database.References)
+
+	referenceUpdates, err := rt.processReferencesLoop(updates)
+	if err != nil {
+		return ModelUpdates{}, ModelUpdates{}, nil, err
+	}
+
+	// merge the updates generated from reference tracking into the main updates
+	err = updates.Merge(rt.dbModel, referenceUpdates)
+	if err != nil {
+		return ModelUpdates{}, ModelUpdates{}, nil, err
+	}
+
+	return updates, referenceUpdates, rt.references, nil
+}
+
+func (rt *referenceTracker) processReferencesLoop(updates ModelUpdates) (ModelUpdates, error) {
+	referenceUpdates := ModelUpdates{}
+
+	// references can be transitive and deleting them can lead to further
+	// references having to be removed so loop until there are no updates to be
+	// made
+	for len(updates.updates) > 0 {
+		// update the references from the updates
+		err := rt.processModelUpdates(updates)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+
+		// process strong reference integrity
+		updates, err = rt.processStrongReferences()
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+
+		// process weak reference integrity
+		weakUpdates, err := rt.processWeakReferences()
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+
+		// merge strong and weak reference updates
+		err = updates.Merge(rt.dbModel, weakUpdates)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+
+		// merge updates from this iteration to the overall reference updates
+		err = referenceUpdates.Merge(rt.dbModel, updates)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+	}
+
+	return referenceUpdates, nil
+}
+
+// processModelUpdates keeps track of the updated references by a set of updates
+func (rt *referenceTracker) processModelUpdates(updates ModelUpdates) error {
+	tables := updates.GetUpdatedTables()
+	for _, table := range tables {
+		err := updates.ForEachRowUpdate(table, func(uuid string, row ovsdb.RowUpdate2) error {
+			return rt.processRowUpdate(table, uuid, &row)
+		})
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// processRowUpdate keeps track of the updated references by a given row update
+func (rt *referenceTracker) processRowUpdate(table, uuid string, row *ovsdb.RowUpdate2) error {
+
+	// getReferencesFromRowModify extracts updated references from the
+	// modifications. Following the same strategy as the modify field of Update2
+	// notification, it will extract a difference, that is, both old removed
+	// references and new added references are extracted. This difference will
+	// then be applied to currently tracked references to come up with the
+	// updated references.
+
+	// For more info on the modify field of Update2 notification and the
+	// strategy used to apply differences, check
+	// https://docs.openvswitch.org/en/latest/ref/ovsdb-server.7/#update2-notification
+
+	var updateRefs database.References
+	switch {
+	case row.Delete != nil:
+		rt.deleted[uuid] = table
+		updateRefs = getReferenceModificationsFromRow(&rt.dbModel, table, uuid, row.Old, row.Old)
+	case row.Modify != nil:
+		updateRefs = getReferenceModificationsFromRow(&rt.dbModel, table, uuid, row.Modify, row.Old)
+	case row.Insert != nil:
+		if !isRoot(&rt.dbModel, table) {
+			// track rows added that are not part of the root set, we might need
+			// to delete those later
+			rt.added[uuid] = table
+			rt.tracked[uuid] = table
+		}
+		updateRefs = getReferenceModificationsFromRow(&rt.dbModel, table, uuid, row.Insert, nil)
+	}
+
+	// (lazy) initialize existing references to the same rows from the database
+	for spec, refs := range updateRefs {
+		for to := range refs {
+			err := rt.initReferences(spec.ToTable, to)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	// apply the reference modifications to the initialized references
+	applyReferenceModifications(rt.references, updateRefs)
+
+	return nil
+}
+
+// processStrongReferences adds delete operations for rows that are not part of
+// the root set and are no longer strongly referenced. Returns a referential
+// integrity violation if a nonexistent row is strongly referenced or a strongly
+// referenced row has been deleted.
+func (rt *referenceTracker) processStrongReferences() (ModelUpdates, error) {
+	// make sure that we are tracking the references to the deleted rows
+	err := rt.initReferencesOfDeletedRows()
+	if err != nil {
+		return ModelUpdates{}, err
+	}
+
+	// track if rows are referenced or not
+	isReferenced := map[string]bool{}
+
+	// go over the updated references
+	for spec, refs := range rt.references {
+
+		// we only care about strong references
+		if !isStrong(&rt.dbModel, spec) {
+			continue
+		}
+
+		for to, from := range refs {
+			// check if the referenced row exists
+			exists, err := rt.rowExists(spec.ToTable, to)
+			if err != nil {
+				return ModelUpdates{}, err
+			}
+			if !exists {
+				for _, uuid := range from {
+					// strong reference to a row that does not exist
+					return ModelUpdates{}, ovsdb.NewReferentialIntegrityViolation(fmt.Sprintf(
+						"Table %s column %s row %s references nonexistent or deleted row %s in table %s",
+						spec.FromTable, spec.FromColumn, uuid, to, spec.ToTable))
+				}
+				// we deleted the row ourselves on a previous loop
+				continue
+			}
+
+			// track if this row is referenced from this location spec
+			isReferenced[to] = isReferenced[to] || len(from) > 0
+		}
+	}
+
+	// inserted rows that are unreferenced and not part of the root set will
+	// silently be dropped from the updates
+	for uuid := range rt.added {
+		if isReferenced[uuid] {
+			continue
+		}
+		isReferenced[uuid] = false
+	}
+
+	// delete rows that are not referenced
+	updates := ModelUpdates{}
+	for uuid, isReferenced := range isReferenced {
+		if isReferenced {
+			// row is still referenced, ignore
+			continue
+		}
+
+		if rt.deleted[uuid] != "" {
+			// already deleted, ignore
+			continue
+		}
+
+		table := rt.tracked[uuid]
+		if isRoot(&rt.dbModel, table) {
+			// table is part of the root set, ignore
+			continue
+		}
+
+		// delete row that is not part of the root set and is no longer
+		// referenced
+		update, err := rt.deleteRow(table, uuid)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+		err = updates.Merge(rt.dbModel, update)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+	}
+
+	return updates, nil
+}
+
+// processWeakReferences deletes weak references to rows that were deleted.
+// Returns a constraint violation if this results in invalid values
+func (rt *referenceTracker) processWeakReferences() (ModelUpdates, error) {
+	// make sure that we are tracking the references to rows that might have
+	// been deleted as a result of strong reference garbage collection
+	err := rt.initReferencesOfDeletedRows()
+	if err != nil {
+		return ModelUpdates{}, err
+	}
+
+	tables := map[string]string{}
+	originalRows := map[string]ovsdb.Row{}
+	updatedRows := map[string]ovsdb.Row{}
+
+	for spec, refs := range rt.references {
+		// fetch some reference information from the schema
+		extendedType, minLenAllowed, refType, _ := refInfo(&rt.dbModel, spec.FromTable, spec.FromColumn, spec.FromValue)
+		isEmptyAllowed := minLenAllowed == 0
+
+		if refType != ovsdb.Weak {
+			// we only care about weak references
+			continue
+		}
+
+		for to, from := range refs {
+			if len(from) == 0 {
+				// not referenced from anywhere, ignore
+				continue
+			}
+
+			// check if the referenced row exists
+			exists, err := rt.rowExists(spec.ToTable, to)
+			if err != nil {
+				return ModelUpdates{}, err
+			}
+			if exists {
+				// we only care about rows that have been deleted or otherwise
+				// don't exist
+				continue
+			}
+
+			// generate the updates to remove the references to deleted rows
+			for _, uuid := range from {
+				if _, ok := updatedRows[uuid]; !ok {
+					updatedRows[uuid] = ovsdb.NewRow()
+				}
+
+				if rt.deleted[uuid] != "" {
+					// already deleted, ignore
+					continue
+				}
+
+				// fetch the original rows
+				if originalRows[uuid] == nil {
+					originalRow, err := rt.getRow(spec.FromTable, uuid)
+					if err != nil {
+						return ModelUpdates{}, err
+					}
+					if originalRow == nil {
+						return ModelUpdates{}, fmt.Errorf("reference from non-existent model with uuid %s", uuid)
+					}
+					originalRows[uuid] = *originalRow
+				}
+
+				var becomesLen int
+				switch extendedType {
+				case ovsdb.TypeMap:
+					// a map referencing the row
+					// generate the mutation to remove the entry form the map
+					originalMap := originalRows[uuid][spec.FromColumn].(ovsdb.OvsMap).GoMap
+					var mutationMap map[interface{}]interface{}
+					value, ok := updatedRows[uuid][spec.FromColumn]
+					if !ok {
+						mutationMap = map[interface{}]interface{}{}
+					} else {
+						mutationMap = value.(ovsdb.OvsMap).GoMap
+					}
+					// copy the map entries referencing the row from the original map
+					mutationMap = copyMapKeyValues(originalMap, mutationMap, !spec.FromValue, ovsdb.UUID{GoUUID: to})
+
+					// track the new length of the map
+					if !isEmptyAllowed {
+						becomesLen = len(originalMap) - len(mutationMap)
+					}
+
+					updatedRows[uuid][spec.FromColumn] = ovsdb.OvsMap{GoMap: mutationMap}
+
+				case ovsdb.TypeSet:
+					// a set referencing the row
+					// generate the mutation to remove the entry form the set
+					var mutationSet []interface{}
+					value, ok := updatedRows[uuid][spec.FromColumn]
+					if !ok {
+						mutationSet = []interface{}{}
+					} else {
+						mutationSet = value.(ovsdb.OvsSet).GoSet
+					}
+					mutationSet = append(mutationSet, ovsdb.UUID{GoUUID: to})
+
+					// track the new length of the set
+					if !isEmptyAllowed {
+						originalSet := originalRows[uuid][spec.FromColumn].(ovsdb.OvsSet).GoSet
+						becomesLen = len(originalSet) - len(mutationSet)
+					}
+
+					updatedRows[uuid][spec.FromColumn] = ovsdb.OvsSet{GoSet: mutationSet}
+
+				case ovsdb.TypeUUID:
+					// this is an atomic UUID value that needs to be cleared
+					updatedRows[uuid][spec.FromColumn] = nil
+					becomesLen = 0
+				}
+
+				if becomesLen < minLenAllowed {
+					return ModelUpdates{}, ovsdb.NewConstraintViolation(fmt.Sprintf(
+						"Deletion of a weak reference to a deleted (or never-existing) row from column %s in table %s "+
+							"row %s caused this column to have an invalid length.",
+						spec.FromColumn, spec.FromTable, uuid))
+				}
+
+				// track the table of the row we are going to update
+				tables[uuid] = spec.FromTable
+			}
+		}
+	}
+
+	// process the updates
+	updates := ModelUpdates{}
+	for uuid, rowUpdate := range updatedRows {
+		update, err := rt.updateRow(tables[uuid], uuid, rowUpdate)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+		err = updates.Merge(rt.dbModel, update)
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+	}
+
+	return updates, nil
+}
+
+func copyMapKeyValues(from, to map[interface{}]interface{}, isKey bool, keyValue ovsdb.UUID) map[interface{}]interface{} {
+	if isKey {
+		to[keyValue] = from[keyValue]
+		return to
+	}
+	for key, value := range from {
+		if value.(ovsdb.UUID) == keyValue {
+			to[key] = from[key]
+		}
+	}
+	return to
+}
+
+// initReferences initializes the references to the provided row from the
+// database
+func (rt *referenceTracker) initReferences(table, uuid string) error {
+	if _, ok := rt.tracked[uuid]; ok {
+		// already initialized
+		return nil
+	}
+	existingRefs, err := rt.provider.GetReferences(rt.dbModel.Client().Name(), table, uuid)
+	if err != nil {
+		return err
+	}
+	rt.references.UpdateReferences(existingRefs)
+	rt.tracked[uuid] = table
+	return nil
+}
+
+func (rt *referenceTracker) initReferencesOfDeletedRows() error {
+	for uuid, table := range rt.deleted {
+		err := rt.initReferences(table, uuid)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// deleteRow adds an update to delete the provided row.
+func (rt *referenceTracker) deleteRow(table, uuid string) (ModelUpdates, error) {
+	model, err := rt.getModel(table, uuid)
+	if err != nil {
+		return ModelUpdates{}, err
+	}
+	row, err := rt.getRow(table, uuid)
+	if err != nil {
+		return ModelUpdates{}, err
+	}
+
+	updates := ModelUpdates{}
+	update := ovsdb.RowUpdate2{Delete: &ovsdb.Row{}, Old: row}
+	err = updates.AddRowUpdate2(rt.dbModel, table, uuid, model, update)
+
+	rt.deleted[uuid] = table
+
+	return updates, err
+}
+
+// updateRow generates updates for the provided row
+func (rt *referenceTracker) updateRow(table, uuid string, row ovsdb.Row) (ModelUpdates, error) {
+	model, err := rt.getModel(table, uuid)
+	if err != nil {
+		return ModelUpdates{}, err
+	}
+
+	// In agreement with processWeakReferences, columns with values are assumed
+	// to be values of sets or maps that need to be mutated for deletion.
+	// Columns with no values are assumed to be atomic optional values that need
+	// to be cleared with an update.
+
+	mutations := make([]ovsdb.Mutation, 0, len(row))
+	update := ovsdb.Row{}
+	for column, value := range row {
+		if value != nil {
+			mutations = append(mutations, *ovsdb.NewMutation(column, ovsdb.MutateOperationDelete, value))
+			continue
+		}
+		update[column] = ovsdb.OvsSet{GoSet: []interface{}{}}
+	}
+
+	updates := ModelUpdates{}
+
+	if len(mutations) > 0 {
+		err = updates.AddOperation(rt.dbModel, table, uuid, model, &ovsdb.Operation{
+			Op:        ovsdb.OperationMutate,
+			Table:     table,
+			Mutations: mutations,
+			Where:     []ovsdb.Condition{ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: uuid})},
+		})
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+	}
+
+	if len(update) > 0 {
+		err = updates.AddOperation(rt.dbModel, table, uuid, model, &ovsdb.Operation{
+			Op:    ovsdb.OperationUpdate,
+			Table: table,
+			Row:   update,
+			Where: []ovsdb.Condition{ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: uuid})},
+		})
+		if err != nil {
+			return ModelUpdates{}, err
+		}
+	}
+
+	return updates, nil
+}
+
+// getModel gets the model from the updates or the database
+func (rt *referenceTracker) getModel(table, uuid string) (model.Model, error) {
+	if _, deleted := rt.deleted[uuid]; deleted {
+		// model has been deleted
+		return nil, nil
+	}
+	// look for the model in the updates
+	model := rt.updates.GetModel(table, uuid)
+	if model != nil {
+		return model, nil
+	}
+	// look for the model in the database
+	model, err := rt.provider.Get(rt.dbModel.Client().Name(), table, uuid)
+	if err != nil {
+		return nil, err
+	}
+	return model, nil
+}
+
+// getRow gets the row from the updates or the database
+func (rt *referenceTracker) getRow(table, uuid string) (*ovsdb.Row, error) {
+	if _, deleted := rt.deleted[uuid]; deleted {
+		// row has been deleted
+		return nil, nil
+	}
+	// look for the row in the updates
+	row := rt.updates.GetRow(table, uuid)
+	if row != nil {
+		return row, nil
+	}
+	// look for the model in the database and build the row
+	model, err := rt.provider.Get(rt.dbModel.Client().Name(), table, uuid)
+	if err != nil {
+		return nil, err
+	}
+	info, err := rt.dbModel.NewModelInfo(model)
+	if err != nil {
+		return nil, err
+	}
+	newRow, err := rt.dbModel.Mapper.NewRow(info)
+	if err != nil {
+		return nil, err
+	}
+	return &newRow, nil
+}
+
+// rowExists returns whether the row exists either in the updates or the database
+func (rt *referenceTracker) rowExists(table, uuid string) (bool, error) {
+	model, err := rt.getModel(table, uuid)
+	return model != nil, err
+}
+
+func getReferenceModificationsFromRow(dbModel *model.DatabaseModel, table, uuid string, modify, old *ovsdb.Row) database.References {
+	refs := database.References{}
+	for column, value := range *modify {
+		var oldValue interface{}
+		if old != nil {
+			oldValue = (*old)[column]
+		}
+		crefs := getReferenceModificationsFromColumn(dbModel, table, uuid, column, value, oldValue)
+		refs.UpdateReferences(crefs)
+	}
+	return refs
+}
+
+func getReferenceModificationsFromColumn(dbModel *model.DatabaseModel, table, uuid, column string, modify, old interface{}) database.References {
+	switch v := modify.(type) {
+	case ovsdb.UUID:
+		var oldUUID ovsdb.UUID
+		if old != nil {
+			oldUUID = old.(ovsdb.UUID)
+		}
+		return getReferenceModificationsFromAtom(dbModel, table, uuid, column, v, oldUUID)
+	case ovsdb.OvsSet:
+		var oldSet ovsdb.OvsSet
+		if old != nil {
+			oldSet = old.(ovsdb.OvsSet)
+		}
+		return getReferenceModificationsFromSet(dbModel, table, uuid, column, v, oldSet)
+	case ovsdb.OvsMap:
+		return getReferenceModificationsFromMap(dbModel, table, uuid, column, v)
+	}
+	return nil
+}
+
+func getReferenceModificationsFromMap(dbModel *model.DatabaseModel, table, uuid, column string, value ovsdb.OvsMap) database.References {
+	if len(value.GoMap) == 0 {
+		return nil
+	}
+
+	// get the referenced table
+	keyRefTable := refTable(dbModel, table, column, false)
+	valueRefTable := refTable(dbModel, table, column, true)
+	if keyRefTable == "" && valueRefTable == "" {
+		return nil
+	}
+
+	from := uuid
+	keySpec := database.ReferenceSpec{ToTable: keyRefTable, FromTable: table, FromColumn: column, FromValue: false}
+	valueSpec := database.ReferenceSpec{ToTable: valueRefTable, FromTable: table, FromColumn: column, FromValue: true}
+
+	refs := database.References{}
+	for k, v := range value.GoMap {
+		if keyRefTable != "" {
+			switch to := k.(type) {
+			case ovsdb.UUID:
+				if _, ok := refs[keySpec]; !ok {
+					refs[keySpec] = database.Reference{to.GoUUID: []string{from}}
+				} else if _, ok := refs[keySpec][to.GoUUID]; !ok {
+					refs[keySpec][to.GoUUID] = append(refs[keySpec][to.GoUUID], from)
+				}
+			}
+		}
+		if valueRefTable != "" {
+			switch to := v.(type) {
+			case ovsdb.UUID:
+				if _, ok := refs[valueSpec]; !ok {
+					refs[valueSpec] = database.Reference{to.GoUUID: []string{from}}
+				} else if _, ok := refs[valueSpec][to.GoUUID]; !ok {
+					refs[valueSpec][to.GoUUID] = append(refs[valueSpec][to.GoUUID], from)
+				}
+			}
+		}
+	}
+
+	return refs
+}
+
+func getReferenceModificationsFromSet(dbModel *model.DatabaseModel, table, uuid, column string, modify, old ovsdb.OvsSet) database.References {
+	// if the modify set is empty, it means the op is clearing an atomic value
+	// so pick the old value instead
+	value := modify
+	if len(modify.GoSet) == 0 {
+		value = old
+	}
+
+	if len(value.GoSet) == 0 {
+		return nil
+	}
+
+	// get the referenced table
+	refTable := refTable(dbModel, table, column, false)
+	if refTable == "" {
+		return nil
+	}
+
+	spec := database.ReferenceSpec{ToTable: refTable, FromTable: table, FromColumn: column}
+	from := uuid
+	refs := database.References{spec: database.Reference{}}
+	for _, v := range value.GoSet {
+		switch to := v.(type) {
+		case ovsdb.UUID:
+			refs[spec][to.GoUUID] = append(refs[spec][to.GoUUID], from)
+		}
+	}
+	return refs
+}
+
+func getReferenceModificationsFromAtom(dbModel *model.DatabaseModel, table, uuid, column string, modify, old ovsdb.UUID) database.References {
+	// get the referenced table
+	refTable := refTable(dbModel, table, column, false)
+	if refTable == "" {
+		return nil
+	}
+	spec := database.ReferenceSpec{ToTable: refTable, FromTable: table, FromColumn: column}
+	from := uuid
+	to := modify.GoUUID
+	refs := database.References{spec: {to: {from}}}
+	if old.GoUUID != "" {
+		// extract the old value as well
+		refs[spec][old.GoUUID] = []string{from}
+	}
+	return refs
+}
+
+// applyReferenceModifications updates references in 'a' from those in 'b'
+func applyReferenceModifications(a, b database.References) {
+	for spec, bv := range b {
+		for to, bfrom := range bv {
+			if av, ok := a[spec]; ok {
+				if afrom, ok := av[to]; ok {
+					r, _ := applyDifference(afrom, bfrom)
+					av[to] = r.([]string)
+				} else {
+					// this reference is not in 'a', so add it
+					av[to] = bfrom
+				}
+			} else {
+				// this reference is not in 'a', so add it
+				a[spec] = database.Reference{to: bfrom}
+			}
+		}
+	}
+}
+
+func refInfo(dbModel *model.DatabaseModel, table, column string, mapValue bool) (ovsdb.ExtendedType, int, ovsdb.RefType, string) {
+	tSchema := dbModel.Schema.Table(table)
+	if tSchema == nil {
+		panic(fmt.Sprintf("unexpected schema error: no schema for table %s", table))
+	}
+
+	cSchema := tSchema.Column(column)
+	if cSchema == nil {
+		panic(fmt.Sprintf("unexpected schema error: no schema for column %s", column))
+	}
+
+	cType := cSchema.TypeObj
+	if cType == nil {
+		// this is not a reference
+		return "", 0, "", ""
+	}
+
+	var bType *ovsdb.BaseType
+	switch {
+	case !mapValue && cType.Key != nil:
+		bType = cType.Key
+	case mapValue && cType.Value != nil:
+		bType = cType.Value
+	default:
+		panic(fmt.Sprintf("unexpected schema error: no schema for map value on column %s", column))
+	}
+	if bType.Type != ovsdb.TypeUUID {
+		// this is not a reference
+		return "", 0, "", ""
+	}
+
+	// treat optional values represented with sets as atomic UUIDs
+	extendedType := cSchema.Type
+	if extendedType == ovsdb.TypeSet && cType.Min() == 0 && cType.Max() == 1 {
+		extendedType = ovsdb.TypeUUID
+	}
+
+	rType, err := bType.RefType()
+	if err != nil {
+		panic(fmt.Sprintf("unexpected schema error: %v", err))
+	}
+
+	rTable, err := bType.RefTable()
+	if err != nil {
+		panic(fmt.Sprintf("unexpected schema error: %v", err))
+	}
+
+	return extendedType, cType.Min(), rType, rTable
+}
+
+func refTable(dbModel *model.DatabaseModel, table, column string, mapValue bool) ovsdb.RefType {
+	_, _, _, refTable := refInfo(dbModel, table, column, mapValue)
+	return refTable
+}
+
+func isRoot(dbModel *model.DatabaseModel, table string) bool {
+	isRoot, err := dbModel.Schema.IsRoot(table)
+	if err != nil {
+		panic(fmt.Sprintf("unexpected schema error: %v", err))
+	}
+	return isRoot
+}
+
+func isStrong(dbModel *model.DatabaseModel, spec database.ReferenceSpec) bool {
+	_, _, refType, _ := refInfo(dbModel, spec.FromTable, spec.FromColumn, spec.FromValue)
+	return refType == ovsdb.Strong
+}
diff --git a/updates/updates.go b/updates/updates.go
index 815cb9e..4ff2363 100644
--- a/updates/updates.go
+++ b/updates/updates.go
@@ -71,8 +71,10 @@ func (u ModelUpdates) GetModel(table, uuid string) model.Model {
 	if u.updates == nil {
 		return nil
 	}
-	if table, found := u.updates[table]; found {
-		return table[uuid].new
+	if t, found := u.updates[table]; found {
+		if update, found := t[uuid]; found {
+			return update.new
+		}
 	}
 	return nil
 }
@@ -83,8 +85,10 @@ func (u ModelUpdates) GetRow(table, uuid string) *ovsdb.Row {
 	if u.updates == nil {
 		return nil
 	}
-	if table, found := u.updates[table]; found {
-		return table[uuid].rowUpdate2.New
+	if t, found := u.updates[table]; found {
+		if update, found := t[uuid]; found {
+			return update.rowUpdate2.New
+		}
 	}
 	return nil
 }
@@ -218,13 +222,21 @@ func (u *ModelUpdates) addUpdate(dbModel model.DatabaseModel, table, uuid string
 		return err
 	}
 
-	// If after the merge this amounts to no update, remove it from the list
-	if update.isEmpty() {
-		delete(u.updates[table], uuid)
+	if !update.isEmpty() {
+		u.updates[table][uuid] = update
 		return nil
 	}
 
-	u.updates[table][uuid] = update
+	// If after the merge this amounts to no update, remove it from the list and
+	// clean up
+	delete(u.updates[table], uuid)
+	if len(u.updates[table]) == 0 {
+		delete(u.updates, table)
+	}
+	if len(u.updates) == 0 {
+		u.updates = nil
+	}
+
 	return nil
 }
 


Test patch:
diff --git a/client/client_test.go b/client/client_test.go
index 1f66651..f6e4da0 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -20,7 +20,7 @@ import (
 	"github.com/go-logr/stdr"
 	"github.com/google/uuid"
 	"github.com/ovn-org/libovsdb/cache"
-	db "github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/database/inmemory"
 	"github.com/ovn-org/libovsdb/mapper"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
@@ -956,7 +956,7 @@ func newOVSDBServer(t *testing.T, dbModel model.ClientDBModel, schema ovsdb.Data
 	require.NoError(t, err)
 	serverSchema := serverdb.Schema()
 
-	db := db.NewInMemoryDatabase(map[string]model.ClientDBModel{
+	db := inmemory.NewDatabase(map[string]model.ClientDBModel{
 		schema.Name:       dbModel,
 		serverSchema.Name: serverDBModel,
 	})
diff --git a/database/transaction_test.go b/database/inmemory/inmemory_test.go
similarity index 62%
rename from database/transaction_test.go
rename to database/inmemory/inmemory_test.go
index 6b4e550..322c021 100644
--- a/database/transaction_test.go
+++ b/database/inmemory/inmemory_test.go
@@ -1,4 +1,4 @@
-package database
+package inmemory
 
 import (
 	"testing"
@@ -8,6 +8,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/ovn-org/libovsdb/database"
 	"github.com/ovn-org/libovsdb/mapper"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
@@ -18,7 +19,7 @@ import (
 func TestWaitOpEquals(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 	m := mapper.NewMapper(dbModel.Schema)
@@ -45,57 +46,57 @@ func TestWaitOpEquals(t *testing.T) {
 	bridgeRow, err := m.NewRow(bridgeInfo)
 	require.Nil(t, err)
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
+	transaction := db.NewTransaction("Open_vSwitch")
 
-	operation := ovsdb.Operation{
-		Op:    ovsdb.OperationInsert,
-		Table: "Open_vSwitch",
-		UUID:  ovsUUID,
-		Row:   ovsRow,
-	}
-	res, updates := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
-	require.Nil(t, err)
-
-	operation = ovsdb.Operation{
-		Op:    ovsdb.OperationInsert,
-		Table: "Bridge",
-		UUID:  bridgeUUID,
-		Row:   bridgeRow,
+	operations := []ovsdb.Operation{
+		{
+			Op:       ovsdb.OperationInsert,
+			Table:    "Open_vSwitch",
+			UUIDName: ovsUUID,
+			Row:      ovsRow,
+		},
+		{
+			Op:       ovsdb.OperationInsert,
+			Table:    "Bridge",
+			UUIDName: bridgeUUID,
+			Row:      bridgeRow,
+		},
 	}
-	res, update2 := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
-	require.Nil(t, err)
-
-	err = updates.Merge(dbModel, *update2)
+	res, updates := transaction.Transact(operations...)
+	_, err = checkOperationResults(res, operations...)
 	require.NoError(t, err)
-	err = db.Commit("Open_vSwitch", uuid.New(), *updates)
+
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
 	require.NoError(t, err)
 
 	timeout := 0
 	// Attempt to wait for row with name foo to appear
-	gotResult := transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name"},
-		"==",
-		[]ovsdb.Row{{"name": "foo"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.Nil(t, err)
+	operation := ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name"},
+		Until:   "==",
+		Rows:    []ovsdb.Row{{"name": "foo"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
 
 	// Attempt to wait for 2 rows, where one does not exist
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name"},
-		"==",
-		[]ovsdb.Row{{"name": "foo"}, {"name": "blah"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.NotNil(t, err)
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name"},
+		Until:   "==",
+		Rows:    []ovsdb.Row{{"name": "foo"}, {"name": "blah"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.Error(t, err)
 
 	extIDs, err := ovsdb.NewOvsMap(map[string]string{
 		"foo":   "bar",
@@ -104,48 +105,53 @@ func TestWaitOpEquals(t *testing.T) {
 	})
 	require.Nil(t, err)
 	// Attempt to wait for a row, with multiple columns specified
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name", "external_ids"},
-		"==",
-		[]ovsdb.Row{{"name": "foo", "external_ids": extIDs}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.Nil(t, err)
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name", "external_ids"},
+		Until:   "==",
+		Rows:    []ovsdb.Row{{"name": "foo", "external_ids": extIDs}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
 
 	// Attempt to wait for a row, with multiple columns, but not specified in row filtering
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name", "external_ids"},
-		"==",
-		[]ovsdb.Row{{"name": "foo"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.Nil(t, err)
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name", "external_ids"},
+		Until:   "==",
+		Rows:    []ovsdb.Row{{"name": "foo"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
 
 	// Attempt to get something with a non-zero timeout that will fail
 	timeout = 400
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name", "external_ids"},
-		"==",
-		[]ovsdb.Row{{"name": "doesNotExist"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.NotNil(t, err)
-
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name", "external_ids"},
+		Until:   "==",
+		Rows:    []ovsdb.Row{{"name": "doesNotExist"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.Error(t, err)
 }
 
 func TestWaitOpNotEquals(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 	m := mapper.NewMapper(dbModel.Schema)
@@ -172,57 +178,57 @@ func TestWaitOpNotEquals(t *testing.T) {
 	bridgeRow, err := m.NewRow(bridgeInfo)
 	require.Nil(t, err)
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
+	transaction := db.NewTransaction("Open_vSwitch")
 
-	operation := ovsdb.Operation{
-		Op:    ovsdb.OperationInsert,
-		Table: "Open_vSwitch",
-		UUID:  ovsUUID,
-		Row:   ovsRow,
-	}
-	res, updates := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
-	require.Nil(t, err)
-
-	operation = ovsdb.Operation{
-		Op:    ovsdb.OperationInsert,
-		Table: "Bridge",
-		UUID:  bridgeUUID,
-		Row:   bridgeRow,
+	operations := []ovsdb.Operation{
+		{
+			Op:       ovsdb.OperationInsert,
+			Table:    "Open_vSwitch",
+			UUIDName: ovsUUID,
+			Row:      ovsRow,
+		},
+		{
+			Op:       ovsdb.OperationInsert,
+			Table:    "Bridge",
+			UUIDName: bridgeUUID,
+			Row:      bridgeRow,
+		},
 	}
-	res, update2 := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
-	require.Nil(t, err)
-
-	err = updates.Merge(dbModel, *update2)
+	res, updates := transaction.Transact(operations...)
+	_, err = checkOperationResults(res, operations...)
 	require.NoError(t, err)
-	err = db.Commit("Open_vSwitch", uuid.New(), *updates)
+
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
 	require.NoError(t, err)
 
 	timeout := 0
 	// Attempt a wait where no entry with name blah should exist
-	gotResult := transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name"},
-		"!=",
-		[]ovsdb.Row{{"name": "blah"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.Nil(t, err)
+	operation := ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name"},
+		Until:   "!=",
+		Rows:    []ovsdb.Row{{"name": "blah"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
 
 	// Attempt another wait with multiple rows specified, one that would match, and one that doesn't
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name"},
-		"!=",
-		[]ovsdb.Row{{"name": "blah"}, {"name": "foo"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.Nil(t, err)
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name"},
+		Until:   "!=",
+		Rows:    []ovsdb.Row{{"name": "blah"}, {"name": "foo"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
 
 	// Attempt another wait where name would match, but ext ids would not match
 	NoMatchExtIDs, err := ovsdb.NewOvsMap(map[string]string{
@@ -230,31 +236,38 @@ func TestWaitOpNotEquals(t *testing.T) {
 		"baz":   "quux",
 		"waldo": "is_different",
 	})
-	require.Nil(t, err)
+	require.NoError(t, err)
+
 	// Attempt to wait for a row, with multiple columns specified and one is not a match
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name", "external_ids"},
-		"!=",
-		[]ovsdb.Row{{"name": "foo", "external_ids": NoMatchExtIDs}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
-	require.Nil(t, err)
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name", "external_ids"},
+		Until:   "!=",
+		Rows:    []ovsdb.Row{{"name": "foo", "external_ids": NoMatchExtIDs}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
 
 	// Check to see if a non match takes around the timeout
 	start := time.Now()
 	timeout = 200
-	gotResult = transaction.Wait(
-		"Bridge",
-		&timeout,
-		[]ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
-		[]string{"name"},
-		"!=",
-		[]ovsdb.Row{{"name": "foo"}},
-	)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{gotResult}, []ovsdb.Operation{{Op: "wait"}})
+	operation = ovsdb.Operation{
+		Op:      ovsdb.OperationWait,
+		Table:   "Bridge",
+		Timeout: &timeout,
+		Where:   []ovsdb.Condition{ovsdb.NewCondition("name", ovsdb.ConditionEqual, "foo")},
+		Columns: []string{"name"},
+		Until:   "!=",
+		Rows:    []ovsdb.Row{{"name": "foo"}},
+	}
+	res, _ = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.Error(t, err)
+
 	ts := time.Since(start)
 	if ts < time.Duration(timeout)*time.Millisecond {
 		t.Fatalf("Should have taken at least %d milliseconds to return, but it took %d instead", timeout, ts)
@@ -265,12 +278,11 @@ func TestWaitOpNotEquals(t *testing.T) {
 func TestMutateOp(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 	m := mapper.NewMapper(dbModel.Schema)
 
-	ovsUUID := uuid.NewString()
 	bridgeUUID := uuid.NewString()
 
 	ovs := OvsType{}
@@ -292,42 +304,41 @@ func TestMutateOp(t *testing.T) {
 	bridgeRow, err := m.NewRow(bridgeInfo)
 	require.Nil(t, err)
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
+	transaction := db.NewTransaction("Open_vSwitch")
 
-	operation := ovsdb.Operation{
-		Op:    ovsdb.OperationInsert,
-		Table: "Open_vSwitch",
-		UUID:  ovsUUID,
-		Row:   ovsRow,
-	}
-	res, updates := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
-	require.Nil(t, err)
-
-	operation = ovsdb.Operation{
-		Op:    ovsdb.OperationInsert,
-		Table: "Bridge",
-		UUID:  bridgeUUID,
-		Row:   bridgeRow,
+	operations := []ovsdb.Operation{
+		{
+			Op:    ovsdb.OperationInsert,
+			Table: "Open_vSwitch",
+			Row:   ovsRow,
+		},
+		{
+			Op:    ovsdb.OperationInsert,
+			Table: "Bridge",
+			UUID:  bridgeUUID,
+			Row:   bridgeRow,
+		},
 	}
-	res, update2 := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
-	require.Nil(t, err)
-
-	err = updates.Merge(dbModel, *update2)
+	res, updates := transaction.Transact(operations...)
+	_, err = checkOperationResults(res, operations...)
 	require.NoError(t, err)
-	err = db.Commit("Open_vSwitch", uuid.New(), *updates)
+
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
 	require.NoError(t, err)
 
-	operation = ovsdb.Operation{
+	ovsUUID := res[0].UUID.GoUUID
+	operation := ovsdb.Operation{
 		Op:        ovsdb.OperationMutate,
 		Table:     "Open_vSwitch",
 		Where:     []ovsdb.Condition{ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: ovsUUID})},
 		Mutations: []ovsdb.Mutation{*ovsdb.NewMutation("bridges", ovsdb.MutateOperationInsert, ovsdb.UUID{GoUUID: bridgeUUID})},
 	}
-	gotResult, gotUpdate := transaction.Mutate(&operation)
-	assert.Equal(t, ovsdb.OperationResult{Count: 1}, gotResult)
-	err = db.Commit("Open_vSwitch", uuid.New(), *gotUpdate)
+	res, updates = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
+	assert.Equal(t, []*ovsdb.OperationResult{{Count: 1}}, res)
+
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
 	require.NoError(t, err)
 
 	bridgeSet, err := ovsdb.NewOvsSet([]ovsdb.UUID{{GoUUID: bridgeUUID}})
@@ -349,7 +360,7 @@ func TestMutateOp(t *testing.T) {
 				},
 			},
 		},
-	}, getTableUpdates(*gotUpdate))
+	}, getTableUpdates(updates))
 
 	keyDelete, err := ovsdb.NewOvsSet([]string{"foo"})
 	assert.Nil(t, err)
@@ -365,8 +376,10 @@ func TestMutateOp(t *testing.T) {
 			*ovsdb.NewMutation("external_ids", ovsdb.MutateOperationDelete, keyValueDelete),
 		},
 	}
-	gotResult, gotUpdate = transaction.Mutate(&operation)
-	assert.Equal(t, ovsdb.OperationResult{Count: 1}, gotResult)
+	res, updates = transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
+	require.NoError(t, err)
+	assert.Equal(t, []*ovsdb.OperationResult{{Count: 1}}, res)
 
 	oldExternalIds, _ := ovsdb.NewOvsMap(bridge.ExternalIds)
 	newExternalIds, _ := ovsdb.NewOvsMap(map[string]string{"waldo": "fred"})
@@ -374,9 +387,9 @@ func TestMutateOp(t *testing.T) {
 
 	assert.Nil(t, err)
 
-	gotModify := *getTableUpdates(*gotUpdate)["Bridge"][bridgeUUID].Modify
-	gotOld := *getTableUpdates(*gotUpdate)["Bridge"][bridgeUUID].Old
-	gotNew := *getTableUpdates(*gotUpdate)["Bridge"][bridgeUUID].New
+	gotModify := *getTableUpdates(updates)["Bridge"][bridgeUUID].Modify
+	gotOld := *getTableUpdates(updates)["Bridge"][bridgeUUID].Old
+	gotNew := *getTableUpdates(updates)["Bridge"][bridgeUUID].New
 	assert.Equal(t, diffExternalIds, gotModify["external_ids"])
 	assert.Equal(t, oldExternalIds, gotOld["external_ids"])
 	assert.Equal(t, newExternalIds, gotNew["external_ids"])
@@ -386,7 +399,7 @@ func TestOvsdbServerInsert(t *testing.T) {
 	t.Skip("need a helper for comparing rows as map elements aren't in same order")
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 	m := mapper.NewMapper(dbModel.Schema)
@@ -408,7 +421,7 @@ func TestOvsdbServerInsert(t *testing.T) {
 	bridgeRow, err := m.NewRow(bridgeInfo)
 	require.Nil(t, err)
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
+	transaction := db.NewTransaction("Open_vSwitch")
 
 	operation := ovsdb.Operation{
 		Op:    ovsdb.OperationInsert,
@@ -416,11 +429,11 @@ func TestOvsdbServerInsert(t *testing.T) {
 		UUID:  bridgeUUID,
 		Row:   bridgeRow,
 	}
-	res, updates := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
+	res, updates := transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
 	require.NoError(t, err)
 
-	err = db.Commit("Open_vSwitch", uuid.New(), *updates)
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
 	assert.NoError(t, err)
 
 	bridge.UUID = bridgeUUID
@@ -440,7 +453,7 @@ func TestOvsdbServerInsert(t *testing.T) {
 func TestOvsdbServerUpdate(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 	m := mapper.NewMapper(dbModel.Schema)
@@ -461,7 +474,7 @@ func TestOvsdbServerUpdate(t *testing.T) {
 	bridgeRow, err := m.NewRow(bridgeInfo)
 	require.Nil(t, err)
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
+	transaction := db.NewTransaction("Open_vSwitch")
 
 	operation := ovsdb.Operation{
 		Op:    ovsdb.OperationInsert,
@@ -469,11 +482,11 @@ func TestOvsdbServerUpdate(t *testing.T) {
 		UUID:  bridgeUUID,
 		Row:   bridgeRow,
 	}
-	res, updates := transaction.Insert(&operation)
-	_, err = ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "insert"}})
+	res, updates := transaction.Transact(operation)
+	_, err = checkOperationResults(res, operation)
 	require.NoError(t, err)
 
-	err = db.Commit("Open_vSwitch", uuid.New(), *updates)
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
 	assert.NoError(t, err)
 
 	halloween, _ := ovsdb.NewOvsSet([]string{"halloween"})
@@ -522,6 +535,7 @@ func TestOvsdbServerUpdate(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			transaction := db.NewTransaction("Open_vSwitch")
 			op := ovsdb.Operation{
 				Op:    ovsdb.OperationUpdate,
 				Table: "Bridge",
@@ -530,8 +544,8 @@ func TestOvsdbServerUpdate(t *testing.T) {
 				}},
 				Row: tt.row,
 			}
-			res, updates := transaction.Update(&op)
-			errs, err := ovsdb.CheckOperationResults([]ovsdb.OperationResult{res}, []ovsdb.Operation{{Op: "update"}})
+			res, updates := transaction.Transact(op)
+			errs, err := checkOperationResults(res, op)
 			require.NoErrorf(t, err, "%+v", errs)
 
 			bridge.UUID = bridgeUUID
@@ -539,7 +553,7 @@ func TestOvsdbServerUpdate(t *testing.T) {
 			assert.NoError(t, err)
 			br := row.(*BridgeType)
 			assert.NotEqual(t, br, bridgeRow)
-			assert.Equal(t, tt.expected.Modify, getTableUpdates(*updates)["Bridge"][bridgeUUID].Modify)
+			assert.Equal(t, tt.expected.Modify, getTableUpdates(updates)["Bridge"][bridgeUUID].Modify)
 		})
 	}
 }
@@ -547,7 +561,7 @@ func TestOvsdbServerUpdate(t *testing.T) {
 func TestMultipleOps(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 
@@ -578,8 +592,8 @@ func TestMultipleOps(t *testing.T) {
 	}
 	ops = append(ops, op)
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
-	results, _ := transaction.Transact(ops)
+	transaction := db.NewTransaction("Open_vSwitch")
+	results, _ := transaction.Transact(ops...)
 	assert.Len(t, results, len(ops))
 	assert.NotNil(t, results[0])
 	assert.Empty(t, results[0].Error)
@@ -616,7 +630,7 @@ func TestMultipleOps(t *testing.T) {
 	}
 	ops = append(ops, op2)
 
-	results, updates := transaction.Transact(ops)
+	results, updates := transaction.Transact(ops...)
 	require.Len(t, results, len(ops))
 	for _, result := range results {
 		assert.Empty(t, result.Error)
@@ -659,11 +673,9 @@ func TestOvsdbServerDbDoesNotExist(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": defDB})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": defDB})
 	err = db.CreateDatabase("Open_vSwitch", schema)
 	require.NoError(t, err)
-	dbModel, errs := model.NewDatabaseModel(schema, defDB)
-	require.Empty(t, errs)
 
 	ops := []ovsdb.Operation{
 		{
@@ -690,8 +702,8 @@ func TestOvsdbServerDbDoesNotExist(t *testing.T) {
 		},
 	}
 
-	transaction := NewTransaction(dbModel, "nonexsitent_db", db, nil)
-	res, _ := transaction.Transact(ops)
+	transaction := db.NewTransaction("nonexsitent_db")
+	res, _ := transaction.Transact(ops...)
 	assert.Len(t, res, len(ops))
 	assert.Equal(t, "database does not exist", res[0].Error)
 	assert.Nil(t, res[1])
@@ -700,7 +712,7 @@ func TestOvsdbServerDbDoesNotExist(t *testing.T) {
 func TestCheckIndexes(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	db := NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
 	require.NoError(t, err)
 
@@ -737,8 +749,8 @@ func TestCheckIndexes(t *testing.T) {
 		},
 	}
 
-	transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
-	results, updates := transaction.Transact(ops)
+	transaction := db.NewTransaction("Open_vSwitch")
+	results, updates := transaction.Transact(ops...)
 	require.Len(t, results, len(ops))
 	for _, result := range results {
 		assert.Equal(t, "", result.Error)
@@ -871,9 +883,9 @@ func TestCheckIndexes(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.desc, func(t *testing.T) {
-			transaction := NewTransaction(dbModel, "Open_vSwitch", db, nil)
+			transaction := db.NewTransaction("Open_vSwitch")
 			ops := tt.ops()
-			results, _ := transaction.Transact(ops)
+			results, _ := transaction.Transact(ops...)
 			var err string
 			for _, result := range results {
 				if result.Error != "" {
@@ -891,7 +903,7 @@ func TestCheckIndexes(t *testing.T) {
 	}
 }
 
-func getTableUpdates(update Update) ovsdb.TableUpdates2 {
+func getTableUpdates(update database.Update) ovsdb.TableUpdates2 {
 	tus := ovsdb.TableUpdates2{}
 	tables := update.GetUpdatedTables()
 	for _, table := range tables {
@@ -904,3 +916,130 @@ func getTableUpdates(update Update) ovsdb.TableUpdates2 {
 	}
 	return tus
 }
+
+func checkOperationResults(result []*ovsdb.OperationResult, ops ...ovsdb.Operation) ([]ovsdb.OperationError, error) {
+	r := make([]ovsdb.OperationResult, len(result))
+	for i := range result {
+		r[i] = *result[i]
+	}
+	return ovsdb.CheckOperationResults(r, ops)
+}
+
+func TestCheckIndexesWithReferentialIntegrity(t *testing.T) {
+	dbModel, err := GetModel()
+	require.NoError(t, err)
+	db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	err = db.CreateDatabase("Open_vSwitch", dbModel.Schema)
+	require.NoError(t, err)
+
+	ovsUUID := uuid.NewString()
+	managerUUID := uuid.NewString()
+	managerUUID2 := uuid.NewString()
+	ops := []ovsdb.Operation{
+		{
+			Table: "Open_vSwitch",
+			Op:    ovsdb.OperationInsert,
+			UUID:  ovsUUID,
+			Row: ovsdb.Row{
+				"manager_options": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: managerUUID}}},
+			},
+		},
+		{
+			Table: "Manager",
+			Op:    ovsdb.OperationInsert,
+			UUID:  managerUUID,
+			Row: ovsdb.Row{
+				"target": "target",
+			},
+		},
+	}
+
+	transaction := db.NewTransaction("Open_vSwitch")
+	results, updates := transaction.Transact(ops...)
+	require.Len(t, results, len(ops))
+	for _, result := range results {
+		assert.Equal(t, "", result.Error)
+	}
+	err = db.Commit("Open_vSwitch", uuid.New(), updates)
+	require.NoError(t, err)
+
+	tests := []struct {
+		desc        string
+		ops         func() []ovsdb.Operation
+		wantUpdates int
+	}{
+		{
+			// As a row is deleted due to garbage collection, that row's index
+			// should be available for use by a different row
+			desc: "Replacing a strong reference should garbage collect and account for indexes",
+			ops: func() []ovsdb.Operation {
+				return []ovsdb.Operation{
+					{
+						Table: "Open_vSwitch",
+						Op:    ovsdb.OperationUpdate,
+						Row: ovsdb.Row{
+							"manager_options": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: managerUUID2}}},
+						},
+						Where: []ovsdb.Condition{
+							ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: ovsUUID}),
+						},
+					},
+					{
+						Table: "Manager",
+						Op:    ovsdb.OperationInsert,
+						UUID:  managerUUID2,
+						Row: ovsdb.Row{
+							"target": "target",
+						},
+					},
+				}
+			},
+			// the update and insert above plus the delete from the garbage
+			// collection
+			wantUpdates: 3,
+		},
+		{
+			desc: "A row that is not root and not strongly referenced should not cause index collisions",
+			ops: func() []ovsdb.Operation {
+				return []ovsdb.Operation{
+					{
+						Table: "Manager",
+						Op:    ovsdb.OperationInsert,
+						UUID:  managerUUID2,
+						Row: ovsdb.Row{
+							"target": "target",
+						},
+					},
+				}
+			},
+			// no updates as the row is not strongly referenced
+			wantUpdates: 0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			transaction := db.NewTransaction("Open_vSwitch")
+			ops := tt.ops()
+			results, update := transaction.Transact(ops...)
+			var err string
+			for _, result := range results {
+				if result.Error != "" {
+					err = result.Error
+					break
+				}
+			}
+			require.Empty(t, err, "got an unexpected error")
+
+			tables := update.GetUpdatedTables()
+			var gotUpdates int
+			for _, table := range tables {
+				_ = update.ForEachRowUpdate(table, func(uuid string, row ovsdb.RowUpdate2) error {
+					gotUpdates++
+					return nil
+				})
+			}
+			assert.Equal(t, tt.wantUpdates, gotUpdates, "got a different number of updates than expected")
+		})
+	}
+}
diff --git a/server/monitor_test.go b/server/monitor_test.go
index deda5ec..27f2ed8 100644
--- a/server/monitor_test.go
+++ b/server/monitor_test.go
@@ -90,7 +90,7 @@ func TestMonitorFilter(t *testing.T) {
 					assert.NoError(t, err)
 				}
 			}
-			tu := monitor.filter2(update)
+			tu := monitor.filter2(updates.NewDatabaseUpdate(update, nil))
 			assert.Equal(t, tt.expected, tu)
 		})
 	}
@@ -175,7 +175,7 @@ func TestMonitorFilter2(t *testing.T) {
 					assert.NoError(t, err)
 				}
 			}
-			tu := monitor.filter2(update)
+			tu := monitor.filter2(updates.NewDatabaseUpdate(update, nil))
 			assert.Equal(t, tt.expected, tu)
 		})
 	}
diff --git a/server/server_integration_test.go b/server/server_integration_test.go
index c070ca8..e4273df 100644
--- a/server/server_integration_test.go
+++ b/server/server_integration_test.go
@@ -10,11 +10,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/uuid"
 	"github.com/ovn-org/libovsdb/cache"
 	"github.com/ovn-org/libovsdb/client"
-	"github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/database/inmemory"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
+	"github.com/ovn-org/libovsdb/test"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -24,7 +26,7 @@ import (
 func buildTestServerAndClient(t *testing.T) (client.Client, func()) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	ovsDB := database.NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	ovsDB := inmemory.NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	schema := dbModel.Schema
 	defDB := dbModel.Client()
 
@@ -516,3 +518,347 @@ func TestMultipleOpsSameRow(t *testing.T) {
 	require.Equal(t, map[string]string{"key1": "value1", "keyA": "valueA"}, br.ExternalIds)
 	require.Nil(t, br.DatapathID)
 }
+
+func TestReferentialIntegrity(t *testing.T) {
+	// UUIDs to use throughout the tests
+	ovsUUID := uuid.New().String()
+	bridgeUUID := uuid.New().String()
+	port1UUID := uuid.New().String()
+	port2UUID := uuid.New().String()
+	mirrorUUID := uuid.New().String()
+
+	// the test adds an additional op to initialOps to set a reference to
+	// the bridge in OVS table
+	// the test deletes expectModels at the end
+	tests := []struct {
+		name             string
+		initialOps       []ovsdb.Operation
+		testOps          func(client.Client) ([]ovsdb.Operation, error)
+		expectModels     []model.Model
+		dontExpectModels []model.Model
+		expectErr        bool
+	}{
+		{
+			name: "strong reference is garbage collected",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name": port1UUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// remove the mirror reference
+				b := &test.BridgeType{UUID: bridgeUUID}
+				return c.Where(b).Update(b, &b.Mirrors)
+			},
+			expectModels: []model.Model{
+				&test.BridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port1UUID}},
+				&test.PortType{UUID: port1UUID, Name: port1UUID},
+			},
+			dontExpectModels: []model.Model{
+				// mirror should have been garbage collected
+				&test.MirrorType{UUID: mirrorUUID},
+			},
+		},
+		{
+			name: "adding non-root row that is not strongly reference is a noop",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name": bridgeUUID,
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// add a mirror
+				m := &test.MirrorType{UUID: mirrorUUID, Name: mirrorUUID}
+				return c.Create(m)
+			},
+			expectModels: []model.Model{
+				&test.BridgeType{UUID: bridgeUUID, Name: bridgeUUID},
+			},
+			dontExpectModels: []model.Model{
+				// mirror should have not been added as is not referenced from anywhere
+				&test.MirrorType{UUID: mirrorUUID},
+			},
+		},
+		{
+			name: "adding non-existent strong reference fails",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name": bridgeUUID,
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// add a mirror
+				b := &test.BridgeType{UUID: bridgeUUID, Mirrors: []string{mirrorUUID}}
+				return c.Where(b).Update(b, &b.Mirrors)
+			},
+			expectModels: []model.Model{
+				&test.BridgeType{UUID: bridgeUUID, Name: bridgeUUID},
+			},
+			expectErr: true,
+		},
+		{
+			name: "weak reference is garbage collected",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}, ovsdb.UUID{GoUUID: port2UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name": port1UUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port2UUID,
+					Row: ovsdb.Row{
+						"name": port2UUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}, ovsdb.UUID{GoUUID: port2UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// remove port1
+				p := &test.PortType{UUID: port1UUID}
+				ops, err := c.Where(p).Delete()
+				if err != nil {
+					return nil, err
+				}
+				b := &test.BridgeType{UUID: bridgeUUID, Ports: []string{port2UUID}}
+				op, err := c.Where(b).Update(b, &b.Ports)
+				if err != nil {
+					return nil, err
+				}
+				return append(ops, op...), nil
+			},
+			expectModels: []model.Model{
+				&test.BridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port2UUID}, Mirrors: []string{mirrorUUID}},
+				&test.PortType{UUID: port2UUID, Name: port2UUID},
+				// mirror reference to port1 should have been garbage collected
+				&test.MirrorType{UUID: mirrorUUID, Name: mirrorUUID, SelectSrcPort: []string{port2UUID}},
+			},
+			dontExpectModels: []model.Model{
+				&test.PortType{UUID: port1UUID},
+			},
+		},
+		{
+			name: "adding a weak reference to a non-existent row is a noop",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name": port1UUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// add reference to non-existent port2
+				m := &test.MirrorType{UUID: mirrorUUID, SelectSrcPort: []string{port1UUID, port2UUID}}
+				return c.Where(m).Update(m, &m.SelectSrcPort)
+			},
+			expectModels: []model.Model{
+				&test.BridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port1UUID}, Mirrors: []string{mirrorUUID}},
+				&test.PortType{UUID: port1UUID, Name: port1UUID},
+				// mirror reference to port2 should have been garbage collected resulting in noop
+				&test.MirrorType{UUID: mirrorUUID, Name: mirrorUUID, SelectSrcPort: []string{port1UUID}},
+			},
+		},
+		{
+			name: "garbage collecting a weak reference on a column lowering it below the min length fails",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name": port1UUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// remove port 1
+				return c.Where(&test.PortType{UUID: port1UUID}).Delete()
+			},
+			expectModels: []model.Model{
+				&test.BridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port1UUID}, Mirrors: []string{mirrorUUID}},
+				&test.PortType{UUID: port1UUID, Name: port1UUID},
+				&test.MirrorType{UUID: mirrorUUID, Name: mirrorUUID, SelectSrcPort: []string{port1UUID}},
+			},
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c, close := buildTestServerAndClient(t)
+			defer close()
+			_, err := c.MonitorAll(context.Background())
+			require.NoError(t, err)
+
+			// add the bridge reference to the initial ops
+			ops := append(tt.initialOps, ovsdb.Operation{
+				Op:    ovsdb.OperationInsert,
+				Table: "Open_vSwitch",
+				UUID:  ovsUUID,
+				Row: ovsdb.Row{
+					"bridges": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: bridgeUUID}}},
+				},
+			})
+
+			results, err := c.Transact(context.Background(), ops...)
+			require.NoError(t, err)
+			require.Len(t, results, len(ops))
+
+			errors, err := ovsdb.CheckOperationResults(results, ops)
+			require.Nil(t, errors)
+			require.NoError(t, err)
+
+			ops, err = tt.testOps(c)
+			require.NoError(t, err)
+
+			results, err = c.Transact(context.Background(), ops...)
+			require.NoError(t, err)
+
+			errors, err = ovsdb.CheckOperationResults(results, ops)
+			require.Nil(t, errors)
+			if tt.expectErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+
+			for _, m := range tt.expectModels {
+				actual := model.Clone(m)
+				err := c.Get(context.Background(), actual)
+				require.NoError(t, err, "when expecting model %v", m)
+				require.Equal(t, m, actual)
+			}
+
+			for _, m := range tt.dontExpectModels {
+				err := c.Get(context.Background(), m)
+				require.ErrorIs(t, err, client.ErrNotFound, "when not expecting model %v", m)
+			}
+
+			ops = []ovsdb.Operation{}
+			for _, m := range tt.expectModels {
+				op, err := c.Where(m).Delete()
+				require.NoError(t, err)
+				require.Len(t, op, 1)
+				ops = append(ops, op...)
+			}
+
+			// remove the bridge reference
+			ops = append(ops, ovsdb.Operation{
+				Op:    ovsdb.OperationDelete,
+				Table: "Open_vSwitch",
+				Where: []ovsdb.Condition{
+					{
+						Column:   "_uuid",
+						Function: ovsdb.ConditionEqual,
+						Value:    ovsdb.UUID{GoUUID: ovsUUID},
+					},
+				},
+			})
+
+			results, err = c.Transact(context.Background(), ops...)
+			require.NoError(t, err)
+			require.Len(t, results, len(ops))
+
+			errors, err = ovsdb.CheckOperationResults(results, ops)
+			require.Nil(t, errors)
+			require.NoError(t, err)
+		})
+	}
+}
diff --git a/server/server_test.go b/server/server_test.go
index 36cf4d8..77d1a59 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/google/uuid"
-	"github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/database/inmemory"
 	"github.com/ovn-org/libovsdb/model"
 	"github.com/ovn-org/libovsdb/ovsdb"
 	"github.com/stretchr/testify/assert"
@@ -17,7 +17,7 @@ import (
 func TestOvsdbServerMonitor(t *testing.T) {
 	dbModel, err := GetModel()
 	require.NoError(t, err)
-	ovsDB := database.NewInMemoryDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
+	ovsDB := inmemory.NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()})
 	schema := dbModel.Schema
 
 	o, err := NewOvsdbServer(ovsDB, dbModel)
@@ -65,8 +65,8 @@ func TestOvsdbServerMonitor(t *testing.T) {
 			Row:   ovsdb.Row{"name": "quux"},
 		},
 	}
-	transaction := database.NewTransaction(dbModel, "Open_vSwitch", o.db, &o.logger)
-	_, updates := transaction.Transact(operations)
+	transaction := ovsDB.NewTransaction("Open_vSwitch")
+	_, updates := transaction.Transact(operations...)
 	err = o.db.Commit("Open_vSwitch", uuid.New(), updates)
 	require.NoError(t, err)
 
diff --git a/test/ovs/ovs_integration_test.go b/test/ovs/ovs_integration_test.go
index daafc92..88da641 100644
--- a/test/ovs/ovs_integration_test.go
+++ b/test/ovs/ovs_integration_test.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/cenkalti/backoff/v4"
+	"github.com/google/uuid"
 	"github.com/ory/dockertest/v3"
 	"github.com/ory/dockertest/v3/docker"
 	"github.com/ovn-org/libovsdb/cache"
@@ -164,6 +165,7 @@ type bridgeType struct {
 	BridgeFailMode *BridgeFailMode   `ovsdb:"fail_mode"`
 	IPFIX          *string           `ovsdb:"ipfix"`
 	DatapathID     *string           `ovsdb:"datapath_id"`
+	Mirrors        []string          `ovsdb:"mirrors"`
 }
 
 // ovsType is the ORM model of the OVS table
@@ -200,11 +202,31 @@ type queueType struct {
 	DSCP *int   `ovsdb:"dscp"`
 }
 
+type portType struct {
+	UUID       string   `ovsdb:"_uuid"`
+	Name       string   `ovsdb:"name"`
+	Interfaces []string `ovsdb:"interfaces"`
+}
+
+type interfaceType struct {
+	UUID string `ovsdb:"_uuid"`
+	Name string `ovsdb:"name"`
+}
+
+type mirrorType struct {
+	UUID          string   `ovsdb:"_uuid"`
+	Name          string   `ovsdb:"name"`
+	SelectSrcPort []string `ovsdb:"select_src_port"`
+}
+
 var defDB, _ = model.NewClientDBModel("Open_vSwitch", map[string]model.Model{
 	"Open_vSwitch": &ovsType{},
 	"Bridge":       &bridgeType{},
 	"IPFIX":        &ipfixType{},
 	"Queue":        &queueType{},
+	"Port":         &portType{},
+	"Mirror":       &mirrorType{},
+	"Interface":    &interfaceType{},
 })
 
 func (suite *OVSIntegrationSuite) TestConnectReconnect() {
@@ -1243,3 +1265,361 @@ func (suite *OVSIntegrationSuite) TestMultipleOpsSameRow() {
 	require.Equal(suite.T(), map[string]string{"key1": "value1", "keyA": "valueA"}, br.ExternalIds)
 	require.Nil(suite.T(), br.DatapathID)
 }
+
+func (suite *OVSIntegrationSuite) TestReferentialIntegrity() {
+	t := suite.Suite.T()
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// fetch the OVS UUID
+	var ovs []*ovsType
+	err := suite.clientWithoutInactvityCheck.WhereCache(func(*ovsType) bool { return true }).List(ctx, &ovs)
+	require.NoError(t, err)
+	require.Len(t, ovs, 1)
+
+	// UUIDs to use throughout the tests
+	ovsUUID := ovs[0].UUID
+	bridgeUUID := uuid.New().String()
+	port1UUID := uuid.New().String()
+	port2UUID := uuid.New().String()
+	interfaceUUID := uuid.New().String()
+	mirrorUUID := uuid.New().String()
+
+	// monitor additional table specific to this test
+	_, err = suite.clientWithoutInactvityCheck.Monitor(ctx,
+		suite.clientWithoutInactvityCheck.NewMonitor(
+			client.WithTable(&portType{}),
+			client.WithTable(&interfaceType{}),
+			client.WithTable(&mirrorType{}),
+		),
+	)
+	require.NoError(t, err)
+
+	// the test adds an additional op to initialOps to set a reference to
+	// the bridge in OVS table
+	// the test deletes expectModels at the end
+	tests := []struct {
+		name             string
+		initialOps       []ovsdb.Operation
+		testOps          func(client.Client) ([]ovsdb.Operation, error)
+		expectModels     []model.Model
+		dontExpectModels []model.Model
+		expectErr        bool
+	}{
+		{
+			name: "strong reference is garbage collected",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name":       port1UUID,
+						"interfaces": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: interfaceUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Interface",
+					UUID:  interfaceUUID,
+					Row: ovsdb.Row{
+						"name": interfaceUUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// remove the mirror reference
+				b := &bridgeType{UUID: bridgeUUID}
+				return c.Where(b).Update(b, &b.Mirrors)
+			},
+			expectModels: []model.Model{
+				&bridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port1UUID}},
+				&portType{UUID: port1UUID, Name: port1UUID, Interfaces: []string{interfaceUUID}},
+				&interfaceType{UUID: interfaceUUID, Name: interfaceUUID},
+			},
+			dontExpectModels: []model.Model{
+				// mirror should have been garbage collected
+				&mirrorType{UUID: mirrorUUID},
+			},
+		},
+		{
+			name: "adding non-root row that is not strongly reference is a noop",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name": bridgeUUID,
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// add a mirror
+				m := &mirrorType{UUID: mirrorUUID, Name: mirrorUUID}
+				return c.Create(m)
+			},
+			expectModels: []model.Model{
+				&bridgeType{UUID: bridgeUUID, Name: bridgeUUID},
+			},
+			dontExpectModels: []model.Model{
+				// mirror should have not been added as is not referenced from anywhere
+				&mirrorType{UUID: mirrorUUID},
+			},
+		},
+		{
+			name: "adding non-existent strong reference fails",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name": bridgeUUID,
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// add a mirror
+				b := &bridgeType{UUID: bridgeUUID, Mirrors: []string{mirrorUUID}}
+				return c.Where(b).Update(b, &b.Mirrors)
+			},
+			expectModels: []model.Model{
+				&bridgeType{UUID: bridgeUUID, Name: bridgeUUID},
+			},
+			expectErr: true,
+		},
+		{
+			name: "weak reference is garbage collected",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}, ovsdb.UUID{GoUUID: port2UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name":       port1UUID,
+						"interfaces": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: interfaceUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port2UUID,
+					Row: ovsdb.Row{
+						"name":       port2UUID,
+						"interfaces": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: interfaceUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Interface",
+					UUID:  interfaceUUID,
+					Row: ovsdb.Row{
+						"name": interfaceUUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}, ovsdb.UUID{GoUUID: port2UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// remove port1
+				b := &bridgeType{UUID: bridgeUUID, Ports: []string{port2UUID}}
+				return c.Where(b).Update(b, &b.Ports)
+			},
+			expectModels: []model.Model{
+				&bridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port2UUID}, Mirrors: []string{mirrorUUID}},
+				&portType{UUID: port2UUID, Name: port2UUID, Interfaces: []string{interfaceUUID}},
+				// mirror reference to port1 should have been garbage collected
+				&mirrorType{UUID: mirrorUUID, Name: mirrorUUID, SelectSrcPort: []string{port2UUID}},
+			},
+			dontExpectModels: []model.Model{
+				&portType{UUID: port1UUID},
+			},
+		},
+		{
+			name: "adding a weak reference to a non-existent row is a noop",
+			initialOps: []ovsdb.Operation{
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Bridge",
+					UUID:  bridgeUUID,
+					Row: ovsdb.Row{
+						"name":    bridgeUUID,
+						"ports":   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+						"mirrors": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: mirrorUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Port",
+					UUID:  port1UUID,
+					Row: ovsdb.Row{
+						"name":       port1UUID,
+						"interfaces": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: interfaceUUID}}},
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Interface",
+					UUID:  interfaceUUID,
+					Row: ovsdb.Row{
+						"name": interfaceUUID,
+					},
+				},
+				{
+					Op:    ovsdb.OperationInsert,
+					Table: "Mirror",
+					UUID:  mirrorUUID,
+					Row: ovsdb.Row{
+						"name":            mirrorUUID,
+						"select_src_port": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: port1UUID}}},
+					},
+				},
+			},
+			testOps: func(c client.Client) ([]ovsdb.Operation, error) {
+				// add reference to non-existent port2
+				m := &mirrorType{UUID: mirrorUUID, SelectSrcPort: []string{port1UUID, port2UUID}}
+				return c.Where(m).Update(m, &m.SelectSrcPort)
+			},
+			expectModels: []model.Model{
+				&bridgeType{UUID: bridgeUUID, Name: bridgeUUID, Ports: []string{port1UUID}, Mirrors: []string{mirrorUUID}},
+				&portType{UUID: port1UUID, Name: port1UUID, Interfaces: []string{interfaceUUID}},
+				&interfaceType{UUID: interfaceUUID, Name: interfaceUUID},
+				// mirror reference to port2 should have been garbage collected resulting in noop
+				&mirrorType{UUID: mirrorUUID, Name: mirrorUUID, SelectSrcPort: []string{port1UUID}},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := suite.clientWithoutInactvityCheck
+
+			// add the bridge reference to the initial ops
+			ops := append(tt.initialOps, ovsdb.Operation{
+				Op:    ovsdb.OperationMutate,
+				Table: "Open_vSwitch",
+				Mutations: []ovsdb.Mutation{
+					{
+						Mutator: ovsdb.MutateOperationInsert,
+						Column:  "bridges",
+						Value:   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: bridgeUUID}}},
+					},
+				},
+				Where: []ovsdb.Condition{
+					{
+						Column:   "_uuid",
+						Function: ovsdb.ConditionEqual,
+						Value:    ovsdb.UUID{GoUUID: ovsUUID},
+					},
+				},
+			})
+
+			results, err := c.Transact(ctx, ops...)
+			require.NoError(t, err)
+			require.Len(t, results, len(ops))
+
+			errors, err := ovsdb.CheckOperationResults(results, ops)
+			require.Nil(t, errors)
+			require.NoError(t, err)
+
+			ops, err = tt.testOps(c)
+			require.NoError(t, err)
+
+			results, err = c.Transact(ctx, ops...)
+			require.NoError(t, err)
+
+			errors, err = ovsdb.CheckOperationResults(results, ops)
+			require.Nil(t, errors)
+			if tt.expectErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+
+			for _, m := range tt.expectModels {
+				actual := model.Clone(m)
+				err := c.Get(ctx, actual)
+				require.NoError(t, err, "when expecting model %v", m)
+				require.Equal(t, m, actual)
+			}
+
+			for _, m := range tt.dontExpectModels {
+				err := c.Get(ctx, m)
+				require.ErrorIs(t, err, client.ErrNotFound, "when expecting model %v", m)
+			}
+
+			ops = []ovsdb.Operation{}
+			for _, m := range tt.expectModels {
+				op, err := c.Where(m).Delete()
+				require.NoError(t, err)
+				require.Len(t, op, 1)
+				ops = append(ops, op...)
+			}
+
+			// remove the bridge reference
+			ops = append(ops, ovsdb.Operation{
+				Op:    ovsdb.OperationMutate,
+				Table: "Open_vSwitch",
+				Mutations: []ovsdb.Mutation{
+					{
+						Mutator: ovsdb.MutateOperationDelete,
+						Column:  "bridges",
+						Value:   ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: bridgeUUID}}},
+					},
+				},
+				Where: []ovsdb.Condition{
+					{
+						Column:   "_uuid",
+						Function: ovsdb.ConditionEqual,
+						Value:    ovsdb.UUID{GoUUID: ovsUUID},
+					},
+				},
+			})
+
+			results, err = c.Transact(context.Background(), ops...)
+			require.NoError(t, err)
+			require.Len(t, results, len(ops))
+
+			errors, err = ovsdb.CheckOperationResults(results, ops)
+			require.Nil(t, errors)
+			require.NoError(t, err)
+		})
+	}
+}
diff --git a/test/test_data.go b/test/test_data.go
index 6be6b8b..e443510 100644
--- a/test/test_data.go
+++ b/test/test_data.go
@@ -8,6 +8,8 @@ import (
 	"github.com/ovn-org/libovsdb/ovsdb"
 )
 
+// Note that this schema is not strictly a subset of the real OVS schema. It has
+// some small variations that allow to effectively test some OVSDB RFC features
 const schema = `
 {
     "name": "Open_vSwitch",
@@ -15,11 +17,20 @@ const schema = `
     "tables": {
         "Open_vSwitch": {
             "columns": {
-                "bridges": {
+                "manager_options": {
                     "type": {
                         "key": {
                             "type": "uuid",
-                            "refTable": "Bridge"
+                            "refTable": "Manager"
+                        },
+                        "min": 0,
+                        "max": "unlimited"
+                    }
+                },
+                "bridges": {
+                    "type": {
+                        "key": {
+                            "type": "uuid"
                         },
                         "min": 0,
                         "max": "unlimited"
@@ -47,10 +58,19 @@ const schema = `
                     "ephemeral": true
                 },
                 "ports": {
+                    "type": {
+                        "key": {
+                            "type": "uuid"
+                        },
+                        "min": 0,
+                        "max": "unlimited"
+                    }
+                },
+                "mirrors": {
                     "type": {
                         "key": {
                             "type": "uuid",
-                            "refTable": "Port"
+                            "refTable": "Mirror"
                         },
                         "min": 0,
                         "max": "unlimited"
@@ -82,6 +102,7 @@ const schema = `
                     }
                 }
             },
+            "isRoot": true,
             "indexes": [
                 [
                     "name"
@@ -104,8 +125,7 @@ const schema = `
                 "bridge": {
                     "type": {
                         "key": {
-                            "type": "uuid",
-                            "refTable": "Bridge"
+                            "type": "uuid"
                         },
                         "min": 1,
                         "max": 1
@@ -127,6 +147,42 @@ const schema = `
                     "bridge"
                 ]
             ]
+        },
+        "Manager": {
+            "columns": {
+                "target": {
+                    "type": "string"
+                }
+            },
+            "indexes": [["target"]]
+        },
+        "Mirror": {
+            "columns": {
+                "name": {
+                    "type": "string"
+                },
+                "select_src_port": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Port",
+                            "refType": "weak"
+                        },
+                        "min": 1,
+                        "max": "unlimited"
+                    }
+                }
+            }
+        },
+        "Port": {
+            "columns": {
+                "name": {
+                    "type": "string",
+                    "mutable": false
+                }
+            },
+            "isRoot": true,
+            "indexes": [["name"]]
         }
     }
 }
@@ -142,12 +198,14 @@ type BridgeType struct {
 	ExternalIds  map[string]string `ovsdb:"external_ids"`
 	Ports        []string          `ovsdb:"ports"`
 	Status       map[string]string `ovsdb:"status"`
+	Mirrors      []string          `ovsdb:"mirrors"`
 }
 
 // OvsType is the simplified ORM model of the Bridge table
 type OvsType struct {
-	UUID    string   `ovsdb:"_uuid"`
-	Bridges []string `ovsdb:"bridges"`
+	UUID           string   `ovsdb:"_uuid"`
+	Bridges        []string `ovsdb:"bridges"`
+	ManagerOptions []string `ovsdb:"manager_options"`
 }
 
 type FlowSampleCollectorSetType struct {
@@ -158,6 +216,22 @@ type FlowSampleCollectorSetType struct {
 	IPFIX       *string           // `ovsdb:"ipfix"`
 }
 
+type ManagerType struct {
+	UUID   string `ovsdb:"_uuid"`
+	Target string `ovsdb:"target"`
+}
+
+type PortType struct {
+	UUID string `ovsdb:"_uuid"`
+	Name string `ovsdb:"name"`
+}
+
+type MirrorType struct {
+	UUID          string   `ovsdb:"_uuid"`
+	Name          string   `ovsdb:"name"`
+	SelectSrcPort []string `ovsdb:"select_src_port"`
+}
+
 func GetModel() (model.DatabaseModel, error) {
 	client, err := model.NewClientDBModel(
 		"Open_vSwitch",
@@ -165,6 +239,9 @@ func GetModel() (model.DatabaseModel, error) {
 			"Open_vSwitch":              &OvsType{},
 			"Bridge":                    &BridgeType{},
 			"Flow_Sample_Collector_Set": &FlowSampleCollectorSetType{},
+			"Manager":                   &ManagerType{},
+			"Mirror":                    &MirrorType{},
+			"Port":                      &PortType{},
 		},
 	)
 	if err != nil {
diff --git a/updates/merge_test.go b/updates/merge_test.go
index 54039f9..1b6318a 100644
--- a/updates/merge_test.go
+++ b/updates/merge_test.go
@@ -828,6 +828,53 @@ func Test_merge(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "update optional field to original empty value after update results in no op",
+			args: args{
+				a: modelUpdate{
+					old: &test.BridgeType{
+						Name: "bridge",
+					},
+					new: &test.BridgeType{
+						Name:       "bridge",
+						DatapathID: &newDatapathID,
+					},
+					rowUpdate2: &ovsdb.RowUpdate2{
+						Old: &ovsdb.Row{
+							"name": "bridge",
+						},
+						New: &ovsdb.Row{
+							"name":        "bridge",
+							"datapath_id": ovsdb.OvsSet{GoSet: []interface{}{newDatapathID}},
+						},
+						Modify: &ovsdb.Row{
+							"datapath_id": ovsdb.OvsSet{GoSet: []interface{}{newDatapathID}},
+						},
+					},
+				},
+				b: modelUpdate{
+					old: &test.BridgeType{
+						Name:       "bridge",
+						DatapathID: &newDatapathID,
+					},
+					new: &test.BridgeType{
+						Name: "bridge",
+					},
+					rowUpdate2: &ovsdb.RowUpdate2{
+						Old: &ovsdb.Row{
+							"name":        "bridge",
+							"datapath_id": ovsdb.OvsSet{GoSet: []interface{}{newDatapathID}},
+						},
+						New: &ovsdb.Row{
+							"name": "bridge",
+						},
+						Modify: &ovsdb.Row{
+							"datapath_id": ovsdb.OvsSet{GoSet: []interface{}{}},
+						},
+					},
+				},
+			},
+		},
 		{
 			name: "update optional field to empty value after update",
 			args: args{
diff --git a/updates/references_test.go b/updates/references_test.go
new file mode 100644
index 0000000..6e820ae
--- /dev/null
+++ b/updates/references_test.go
@@ -0,0 +1,2195 @@
+package updates
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ovn-org/libovsdb/database"
+	"github.com/ovn-org/libovsdb/model"
+	"github.com/ovn-org/libovsdb/ovsdb"
+)
+
+const referencesTestSchema = `
+{
+    "name": "References_Test",
+    "version": "0.0.1",
+    "tables": {
+        "Parent": {
+            "columns": {
+                "strong_atomic_required_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child"
+                        },
+                        "min": 1,
+                        "max": 1
+                    }
+                },
+                "strong_atomic_optional_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child"
+                        },
+                        "min": 0,
+                        "max": 1
+                    }
+                },
+                "strong_set_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child"
+                        },
+                        "min": 0,
+                        "max": "unlimited"
+                    }
+                },
+                "strong_map_key_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child"
+                        },
+                        "value": {
+                            "type": "string"
+                        },
+                        "min": 0,
+                        "max": "unlimited"
+                    }
+                },
+                "strong_map_value_reference": {
+                    "type": {
+                        "key": {
+                            "type": "string"
+                        },
+                        "value": {
+                            "type": "uuid",
+                            "refTable": "Child"
+                        },
+                        "min": 1,
+                        "max": "unlimited"
+                    }
+                },
+                "weak_atomic_required_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "weak"
+                        },
+                        "min": 1,
+                        "max": 1
+                    }
+                },
+                "weak_atomic_optional_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "weak"
+                        },
+                        "min": 0,
+                        "max": 1
+                    }
+                },
+                "weak_set_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "weak"
+                        },
+                        "min": 2,
+                        "max": "unlimited"
+                    }
+                },
+                "weak_map_key_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "weak"
+                        },
+                        "value": {
+                            "type": "string"
+                        },
+                        "min": 1,
+                        "max": "unlimited"
+                    }
+                },
+                "weak_map_value_reference": {
+                    "type": {
+                        "key": {
+                            "type": "string"
+                        },
+                        "value": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "weak"
+                        },
+                        "min": 1,
+                        "max": "unlimited"
+                    }
+                },
+                "map_key_value_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "weak"
+                        },
+                        "value": {
+                            "type": "uuid",
+                            "refTable": "Child",
+                            "refType": "strong"
+                        },
+                        "min": 0,
+                        "max": "unlimited"
+                    }
+                }
+            },
+            "isRoot": true
+        },
+        "Child": {
+            "columns": {
+                "name": {
+                    "type": "string",
+                    "mutable": false
+                },
+                "strong_atomic_optional_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Grandchild"
+                        },
+                        "min": 0,
+                        "max": 1
+                    }
+                },
+                "weak_atomic_optional_reference": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Grandchild",
+                            "refType": "weak"
+                        },
+                        "min": 0,
+                        "max": 1
+                    }
+                }
+            },
+            "indexes": [
+                [
+                    "name"
+                ]
+            ]
+        },
+        "Grandchild": {
+            "columns": {
+                "name": {
+                    "type": "string",
+                    "mutable": false
+                }
+            },
+            "indexes": [
+                [
+                    "name"
+                ]
+            ]
+        }
+    }
+}
+`
+
+type Parent struct {
+	UUID                          string            `ovsdb:"_uuid"`
+	StrongAtomicRequiredReference string            `ovsdb:"strong_atomic_required_reference"`
+	StrongAtomicOptionalReference *string           `ovsdb:"strong_atomic_optional_reference"`
+	StrongSetReference            []string          `ovsdb:"strong_set_reference"`
+	StrongMapKeyReference         map[string]string `ovsdb:"strong_map_key_reference"`
+	StrongMapValueReference       map[string]string `ovsdb:"strong_map_value_reference"`
+	WeakAtomicRequiredReference   string            `ovsdb:"weak_atomic_required_reference"`
+	WeakAtomicOptionalReference   *string           `ovsdb:"weak_atomic_optional_reference"`
+	WeakSetReference              []string          `ovsdb:"weak_set_reference"`
+	WeakMapKeyReference           map[string]string `ovsdb:"weak_map_key_reference"`
+	WeakMapValueReference         map[string]string `ovsdb:"weak_map_value_reference"`
+	MapKeyValueReference          map[string]string `ovsdb:"map_key_value_reference"`
+}
+
+type Child struct {
+	UUID                          string  `ovsdb:"_uuid"`
+	StrongAtomicOptionalReference *string `ovsdb:"strong_atomic_optional_reference"`
+	WeakAtomicOptionalReference   *string `ovsdb:"weak_atomic_optional_reference"`
+}
+
+type Grandchild struct {
+	UUID string `ovsdb:"_uuid"`
+}
+
+func getReferencesTestDBModel() (model.DatabaseModel, error) {
+	client, err := model.NewClientDBModel(
+		"References_Test",
+		map[string]model.Model{
+			"Parent":     &Parent{},
+			"Child":      &Child{},
+			"Grandchild": &Grandchild{},
+		},
+	)
+	if err != nil {
+		return model.DatabaseModel{}, err
+	}
+	schema, err := getReferencesTestSchema()
+	if err != nil {
+		return model.DatabaseModel{}, err
+	}
+	dbModel, errs := model.NewDatabaseModel(schema, client)
+	if len(errs) > 0 {
+		return model.DatabaseModel{}, fmt.Errorf("errors build model: %v", errs)
+	}
+	return dbModel, nil
+}
+
+func getReferencesTestSchema() (ovsdb.DatabaseSchema, error) {
+	var dbSchema ovsdb.DatabaseSchema
+	err := json.Unmarshal([]byte(referencesTestSchema), &dbSchema)
+	return dbSchema, err
+}
+
+type testReferenceProvider struct {
+	models     map[string]model.Model
+	references database.References
+}
+
+func (rp *testReferenceProvider) GetReferences(database, table, uuid string) (database.References, error) {
+	return rp.references.GetReferences(table, uuid), nil
+}
+
+func (rp *testReferenceProvider) Get(database, table string, uuid string) (model.Model, error) {
+	return rp.models[uuid], nil
+}
+
+var (
+	referencesTestDBModel model.DatabaseModel
+)
+
+func ptr(s string) *string {
+	return &s
+}
+
+type testData struct {
+	existingModels        []model.Model
+	updatedModels         []model.Model
+	finalModels           []model.Model
+	existingReferences    database.References
+	wantUpdatedReferences database.References
+}
+
+func TestProcessReferences(t *testing.T) {
+	var err error
+	referencesTestDBModel, err = getReferencesTestDBModel()
+	if err != nil {
+		t.Errorf("error building DB model: %v", err)
+	}
+
+	tests := []struct {
+		name     string
+		testData testData
+		wantErr  bool
+	}{
+		{
+			// when a strong reference is replaced with another in a required atomic
+			// field, the referenced row should be deleted
+			name:     "strong atomic required reference garbage collected when replaced",
+			testData: strongAtomicRequiredReferenceTestData(),
+		},
+		{
+			// attempting to delete a row that is strongly referenced from a
+			// required atomic field should fail
+			name:     "constraint violation when strongly referenced row from required field deleted",
+			testData: strongAtomicRequiredReferenceDeleteConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to add a required strong reference to a nonexistent row should
+			// fail
+			name:     "constraint violation when strong required reference to nonexistent row added",
+			testData: strongAtomicRequiredReferenceAddConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// when a strong reference is removed from an optional atomic field, the
+			// referenced row should be deleted
+			name:     "strong atomic optional reference garbage collected when removed",
+			testData: strongAtomicOptionalReferenceTestData(),
+		},
+		{
+			// attempting to delete a row that is strongly referenced from an
+			// optional atomic field should fail
+			name:     "constraint violation when strongly referenced row from optional field deleted",
+			testData: strongAtomicOptionalReferenceDeleteConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to add a optional strong reference to a nonexistent
+			// row should fail
+			name:     "constraint violation when strong optional reference to nonexistent row added",
+			testData: strongAtomicOptionalReferenceAddConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// when a strong reference is removed from a set, the referenced row should
+			// be deleted
+			name:     "strong reference garbage collected when removed from set",
+			testData: strongSetReferenceTestData(),
+		},
+		{
+			// attempting to remove a row that is still strongly referenced in a set should fail
+			name:     "strong set reference constraint violation when row deleted error",
+			testData: strongSetReferenceDeleteConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to add strong set reference to non existent row should fail
+			name:     "strong set reference constraint violation when nonexistent reference added error",
+			testData: strongSetReferenceAddConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// when a strong reference is removed from a map key, the
+			// referenced row should be deleted
+			name:     "strong reference garbage collected when removed from map key",
+			testData: strongMapKeyReferenceTestData(),
+		},
+		{
+			// attempting to remove a row that is still strongly referenced in a
+			// map key should fail
+			name:     "strong map key reference constraint violation when row deleted error",
+			testData: strongMapKeyReferenceDeleteConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to add strong map key reference to non existent row should fail
+			name:     "strong map key reference constraint violation when nonexistent reference added error",
+			testData: strongMapKeyReferenceAddConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// when a strong reference is removed from a map value, the
+			// referenced row should be deleted
+			name:     "strong reference garbage collected when removed from map value",
+			testData: strongMapValueReferenceTestData(),
+		},
+		{
+			// attempting to remove a row that is still strongly referenced in a
+			// map value should fail
+			name:     "strong map value reference constraint violation when row deleted error",
+			testData: strongMapValueReferenceDeleteConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to add strong map value reference to non existent row should fail
+			name:     "strong map value reference constraint violation when nonexistent reference added error",
+			testData: strongMapValueReferenceAddConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// when a weak referenced row is deleted, the reference on an atomic
+			// optional field is also deleted
+			name:     "weak atomic optional reference deleted when row deleted",
+			testData: weakAtomicOptionalReferenceTestData(),
+		},
+		{
+			// when a weak referenced row is deleted, the reference on an set is
+			// also deleted
+			name:     "weak reference deleted from set when row deleted",
+			testData: weakSetReferenceTestData(),
+		},
+		{
+			// when a weak referenced row is deleted, the reference on a map
+			// key is also deleted
+			name:     "weak reference deleted from map key when row deleted",
+			testData: weakMapKeyReferenceTestData(),
+		},
+		{
+			// when a weak referenced row is deleted, the reference on a map
+			// value is also deleted
+			name:     "weak reference deleted from map value when row deleted",
+			testData: weakMapValueReferenceTestData(),
+		},
+		{
+			// attempting to delete a weak referenced row when it is referenced
+			// from an atomic required field will fail
+			name:     "weak reference constraint violation in required atomic field when row deleted error",
+			testData: weakAtomicReferenceConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to delete a weak referenced row when it is referenced
+			// from an set that then becomes smaller than the minimum allowed
+			// will fail
+			name:     "weak reference constraint violation in set becoming smaller than allowed error",
+			testData: weakSetReferenceConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to delete a weak referenced row when it is referenced
+			// from a map key that then becomes smaller than the minimum
+			// allowed will fail
+			name:     "weak reference constraint violation in map key field becoming smaller than allowed error",
+			testData: weakMapKeyReferenceConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// attempting to delete a weak referenced row when it is referenced
+			// from a map value that then becomes smaller than the minimum
+			// allowed will fail
+			name:     "weak reference constraint violation in map value field becoming smaller than allowed error",
+			testData: weakMapValueReferenceConstraintViolationErrorTestData(),
+			wantErr:  true,
+		},
+		{
+			// testing behavior with multiple combinations of references
+			name:     "multiple strong and weak reference changes",
+			testData: multipleReferencesTestData(),
+		},
+		{
+			// corner case
+			// inserting a row in a table that is not part of the root set and
+			// is not strongly referenced is a noop
+			name:     "insert unreferenced row in non root set table is a noop",
+			testData: insertNoRootUnreferencedRowTestData(),
+		},
+		{
+			// corner case
+			// adding a weak reference to a nonexistent row is a noop
+			name:     "insert weak reference to nonexistent row is a noop",
+			testData: weakReferenceToNonExistentRowTestData(),
+		},
+		{
+			// corner case
+			// for a map holding weak key references to strong value references, when
+			// the weak reference row is deleted, the map entry and the strongly
+			// referenced row is also deleted
+			name:     "map with key weak reference and value strong reference, weak reference and strong referenced row deleted",
+			testData: mapKeyValueReferenceTestData(),
+			wantErr:  false,
+		},
+		{
+			// corner case
+			// when a weak referenced row is deleted, multiple references on a map
+			// value are also deleted
+			name:     "multiple weak references deleted from map value when row deleted",
+			testData: multipleWeakMapValueReferenceTestData(),
+			wantErr:  false,
+		},
+		{
+			// corner case when multiple rows are transitively & strongly
+			// referenced, garbage collection happens transitively as well
+			name:     "transitive strong references garbage collected when removed",
+			testData: transitiveStrongReferenceTestData(),
+		},
+		{
+			// corner case
+			// when a strong referenced is removed, an unreferenced row will be
+			// garbage collected and weak references to it removed
+			name:     "transitive strong and weak references garbage collected when removed",
+			testData: transitiveStrongAndWeakReferenceTestData(),
+		},
+		{
+			// corner case
+			// a row needs to have a weak reference garbage collected and
+			// at the same time that row itself is garbage collected due to not
+			// being strongly referenced
+			name:     "strong and weak garbage collection over the same row doesn't fail",
+			testData: sameRowStrongAndWeakReferenceTestData(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			td := tt.testData
+			rp := testReferenceProvider{
+				models:     indexModels(td.existingModels),
+				references: td.existingReferences,
+			}
+
+			onUpdates, err := getUpdates(td.existingModels, td.updatedModels)
+			require.NoError(t, err, "failed to build updates from existing and updated models")
+
+			// need a copy easiest way to have it is generating the updates all
+			// over again
+			onUpdatesCopy, err := getUpdates(td.existingModels, td.updatedModels)
+			require.NoError(t, err, "failed to build updates copy from existing and updated models")
+
+			gotModelUpdates, gotReferenceModelUpdates, gotReferenceUpdates, err := ProcessReferences(referencesTestDBModel, &rp, onUpdates)
+			if tt.wantErr {
+				assert.NotNil(t, err, "expected an error but got none")
+				return
+			}
+			assert.NoError(t, err, "got a different error than expected")
+
+			//gotModelUpdates := gotUpdates.(modelUpdatesWithReferences).ModelUpdates
+			wantModelUpdates, err := getUpdates(td.existingModels, td.finalModels)
+			require.NoError(t, err, "failed to build updates from existing and final models")
+			assert.Equal(t, wantModelUpdates, gotModelUpdates, "got different updates than expected")
+
+			//gotUpdatedReferences := gotUpdates.(modelUpdatesWithReferences).references
+			assert.Equal(t, td.wantUpdatedReferences, gotReferenceUpdates, "got different reference updates than expected")
+
+			gotMergedModelUpdates := onUpdatesCopy
+			err = gotMergedModelUpdates.Merge(referencesTestDBModel, gotReferenceModelUpdates)
+			require.NoError(t, err)
+			assert.Equal(t, gotModelUpdates, gotMergedModelUpdates,
+				"the updates are not a result of merging the initial updates with the reference updates")
+		})
+	}
+}
+
+func getUUID(model model.Model) string {
+	return reflect.ValueOf(model).Elem().FieldByName("UUID").Interface().(string)
+}
+
+func indexModels(models []model.Model) map[string]model.Model {
+	indexed := map[string]model.Model{}
+	for _, model := range models {
+		indexed[getUUID(model)] = model
+	}
+	return indexed
+}
+
+// getUpdates returns the updates needed to go from existing to updated
+func getUpdates(existing, updated []model.Model) (ModelUpdates, error) {
+	// index the models by uuid
+	existingModels := indexModels(existing)
+	updatedModels := indexModels(updated)
+
+	// helpers
+	tables := map[string]string{}
+	getRow := func(model model.Model, fields ...interface{}) (ovsdb.Row, error) {
+		info, err := referencesTestDBModel.NewModelInfo(model)
+		if err != nil {
+			return nil, err
+		}
+		row, err := referencesTestDBModel.Mapper.NewRow(info, fields...)
+		if err != nil {
+			return nil, err
+		}
+		tables[getUUID(model)] = info.Metadata.TableName
+		return row, nil
+	}
+
+	getUpdateOp := func(old, new model.Model) (ovsdb.Operation, error) {
+		var err error
+		var row ovsdb.Row
+
+		// insert
+		if old == nil {
+			row, err := getRow(new)
+			return ovsdb.Operation{
+				Op:    ovsdb.OperationInsert,
+				Table: tables[getUUID(new)],
+				Row:   row,
+			}, err
+		}
+
+		// delete
+		if new == nil {
+			// lazy, just to cache the table of the row
+			_, err := getRow(old)
+
+			return ovsdb.Operation{
+				Op:    ovsdb.OperationDelete,
+				Table: tables[getUUID(old)],
+				Where: []ovsdb.Condition{ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: getUUID(old)})},
+			}, err
+		}
+
+		// update, just with the fields that have been changed
+		fields := []interface{}{}
+		xv := reflect.ValueOf(new).Elem()
+		xt := xv.Type()
+		for i := 0; i < xt.NumField(); i++ {
+			if !reflect.DeepEqual(xv.Field(i).Interface(), reflect.ValueOf(old).Elem().Field(i).Interface()) {
+				fields = append(fields, xv.Field(i).Addr().Interface())
+			}
+		}
+
+		row, err = getRow(new, fields...)
+		return ovsdb.Operation{
+			Op:    ovsdb.OperationUpdate,
+			Table: tables[getUUID(new)],
+			Row:   row,
+			Where: []ovsdb.Condition{ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: getUUID(new)})},
+		}, err
+
+	}
+
+	// generate updates
+	updates := ModelUpdates{}
+	for uuid, updatedModel := range updatedModels {
+		op, err := getUpdateOp(existingModels[uuid], updatedModel)
+		if err != nil {
+			return updates, err
+		}
+		err = updates.AddOperation(referencesTestDBModel, tables[uuid], uuid, existingModels[uuid], &op)
+		if err != nil {
+			return updates, err
+		}
+	}
+
+	// deletes
+	for uuid := range existingModels {
+		if updatedModels[uuid] != nil {
+			continue
+		}
+		op, err := getUpdateOp(existingModels[uuid], nil)
+		if err != nil {
+			return updates, err
+		}
+		err = updates.AddOperation(referencesTestDBModel, tables[uuid], uuid, existingModels[uuid], &op)
+		if err != nil {
+			return updates, err
+		}
+	}
+
+	return updates, nil
+}
+
+func strongAtomicRequiredReferenceTestData() testData {
+	// when a strong reference is replaced with another in a required atomic
+	// field, the referenced row should be deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicRequiredReference: "child",
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// newChild is added and parent reference is replaced with newChild
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicRequiredReference: "newChild",
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "newChild",
+			},
+		},
+		// child model should be deleted as it is no longer referenced
+		finalModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicRequiredReference: "newChild",
+			},
+			&Child{
+				UUID: "newChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_required_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced, newChild is
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_required_reference",
+			}: database.Reference{
+				"child":    nil,
+				"newChild": []string{"parent"},
+			},
+		},
+	}
+}
+
+func strongAtomicRequiredReferenceDeleteConstraintViolationErrorTestData() testData {
+	// attempting to delete a row that is strongly referenced from a required
+	// atomic field should fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicRequiredReference: "child",
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is removed but will fail as it is still referenced
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicRequiredReference: "child",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_required_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func strongAtomicRequiredReferenceAddConstraintViolationErrorTestData() testData {
+	// attempting to add a required strong reference to a nonexistent row should
+	// fail
+	return testData{
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicRequiredReference: "child",
+			},
+		},
+	}
+}
+
+func strongAtomicOptionalReferenceTestData() testData {
+	// when a strong reference is removed from an optional atomic field, the
+	// referenced row should be deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// parent reference to child is removed
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child model should be deleted as it is no longer referenced
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func strongAtomicOptionalReferenceDeleteConstraintViolationErrorTestData() testData {
+	// attempting to delete a row that is strongly referenced from an optional
+	// atomic field should fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is removed but will fail as it is still referenced
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func strongAtomicOptionalReferenceAddConstraintViolationErrorTestData() testData {
+	// attempting to add a optional strong reference to a nonexistent row should
+	// fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		// add reference to child but will fail as it does not exist
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+			},
+		},
+	}
+}
+
+func strongSetReferenceTestData() testData {
+	// when a strong reference is removed from a set, the referenced row should
+	// be deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"child", "otherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child reference is removed from the set
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"otherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child model should be deleted as it is no longer referenced
+		finalModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"otherChild"},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_set_reference",
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_set_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func strongMapKeyReferenceTestData() testData {
+	// when a strong reference is removed from a map key, the referenced row
+	// should be deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapKeyReference: map[string]string{
+					"child": "value",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child reference is removed from the map
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child model should be deleted as it is no longer referenced
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_map_key_reference",
+				FromValue:  false,
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_map_key_reference",
+				FromValue:  false,
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func strongMapKeyReferenceDeleteConstraintViolationErrorTestData() testData {
+	// attempting to remove a row that is still strongly referenced in a map key
+	// should fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapKeyReference: map[string]string{
+					"child": "value",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is removed but will fail as it is still referenced
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapKeyReference: map[string]string{
+					"child": "value",
+				},
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_map_key_reference",
+				FromValue:  false,
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func strongMapKeyReferenceAddConstraintViolationErrorTestData() testData {
+	// attempting to add a map key strong reference to a nonexistent row should
+	// fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		// child reference is added to the map but wil fail as child does not
+		// exist
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                  "parent",
+				StrongMapKeyReference: map[string]string{"child": "value"},
+			},
+		},
+	}
+}
+
+func strongMapValueReferenceTestData() testData {
+	// when a strong reference is removed from a map value, the referenced row
+	// should be deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapValueReference: map[string]string{
+					"key1": "child",
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child reference is removed from the map
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapValueReference: map[string]string{
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child model should be deleted as it is no longer referenced
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapValueReference: map[string]string{
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func strongMapValueReferenceDeleteConstraintViolationErrorTestData() testData {
+	// attempting to remove a row that is still strongly referenced in a map value
+	// should fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapKeyReference: map[string]string{
+					"key": "child",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is removed but will fail as it is still referenced
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				StrongMapKeyReference: map[string]string{
+					"key": "child",
+				},
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func strongMapValueReferenceAddConstraintViolationErrorTestData() testData {
+	// attempting to add a map key strong reference to a nonexistent row should
+	// fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		// child reference is added to the map but wil fail as is it doesn't exist
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                    "parent",
+				StrongMapValueReference: map[string]string{"key": "child"},
+			},
+		},
+	}
+}
+
+func strongSetReferenceDeleteConstraintViolationErrorTestData() testData {
+	// attempting to remove a row that is still strongly referenced should fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"child", "otherChild"},
+			},
+			&Parent{
+				UUID:               "otherParent",
+				StrongSetReference: []string{"child"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child is deleted from parent but will fail as it is still referenced
+		// from other parent
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"otherChild"},
+			},
+			&Parent{
+				UUID:               "otherParent",
+				StrongSetReference: []string{"child"},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_set_reference",
+			}: database.Reference{
+				"child":      []string{"parent", "otherParent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+	}
+}
+
+func strongSetReferenceAddConstraintViolationErrorTestData() testData {
+	// attempting to add strong reference to non existent row should fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"child"},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// otherChild reference is added to parent but will fail as otherChild
+		// does not exist
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:               "parent",
+				StrongSetReference: []string{"child", "otherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_set_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func weakAtomicOptionalReferenceTestData() testData {
+	// when a weak referenced row is deleted, the reference on an atomic
+	// optional field is also deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                        "parent",
+				WeakAtomicOptionalReference: ptr("child"),
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is deleted
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                        "parent",
+				WeakAtomicOptionalReference: ptr("child"),
+			},
+		},
+		// the reference to child should be removed from parent
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func weakAtomicReferenceConstraintViolationErrorTestData() testData {
+	// an attempt to delete a weak referenced row when it is referenced from an
+	// atomic required field will fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                        "parent",
+				WeakAtomicRequiredReference: "child",
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is deleted, but will fail because that would leave a mandatory
+		// field empty
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                        "parent",
+				WeakAtomicRequiredReference: "child",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_atomic_required_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func weakSetReferenceTestData() testData {
+	// when a weak referenced row is deleted, the reference on an set is also
+	// deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:             "parent",
+				WeakSetReference: []string{"child", "otherChild", "thirdChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+			&Child{
+				UUID: "thirdChild",
+			},
+		},
+		// child is deleted
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:             "parent",
+				WeakSetReference: []string{"child", "otherChild", "thirdChild"},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+			&Child{
+				UUID: "thirdChild",
+			},
+		},
+		// the reference to child should be removed from parent
+		finalModels: []model.Model{
+			&Parent{
+				UUID:             "parent",
+				WeakSetReference: []string{"otherChild", "thirdChild"},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+			&Child{
+				UUID: "thirdChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_set_reference",
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+				"thirdChild": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_set_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func weakSetReferenceConstraintViolationErrorTestData() testData {
+	// an attempt to delete a weak referenced row when it is referenced from a
+	// set that then becomes smaller than the minimum allowed will fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:             "parent",
+				WeakSetReference: []string{"child", "otherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child is deleted but will fail because the set becomes empty and
+		// that is not allowed by the schema
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:             "parent",
+				WeakSetReference: []string{"child", "otherChild"},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_set_reference",
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+	}
+}
+
+func weakMapKeyReferenceTestData() testData {
+	// when a weak referenced row is deleted, the reference on a map
+	// value is also deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapKeyReference: map[string]string{
+					"child":      "value1",
+					"otherChild": "value2",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child is deleted
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapKeyReference: map[string]string{
+					"child":      "value1",
+					"otherChild": "value2",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// the reference to child should be removed from parent
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapKeyReference: map[string]string{
+					"otherChild": "value2",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_key_reference",
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_key_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func weakMapValueReferenceTestData() testData {
+	// when a weak referenced row is deleted, the reference on a map
+	// value is also deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key1": "child",
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child is deleted
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key1": "child",
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// the reference to child should be removed from parent
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+func weakMapKeyReferenceConstraintViolationErrorTestData() testData {
+	// an attempt to delete a weak referenced row when it is referenced from a
+	// map key that then becomes smaller than the minimum allowed will fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapKeyReference: map[string]string{
+					"child": "value",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is deleted but will fail because the map becomes empty and
+		// that is not allowed by the schema
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapKeyReference: map[string]string{
+					"child": "value",
+				},
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_key_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func weakMapValueReferenceConstraintViolationErrorTestData() testData {
+	// an attempt to delete a weak referenced row when it is referenced from a
+	// map value that then becomes smaller than the minimum allowed will fail
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key1": "child",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// child is deleted but will fail because the map becomes empty and
+		// that is not allowed by the schema
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key1": "child",
+				},
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+	}
+}
+
+func mapKeyValueReferenceTestData() testData {
+	// for a map holding weak key references to strong value references, when
+	// the weak reference row is deleted, the map entry and the strongly
+	// referenced row is also deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				MapKeyValueReference: map[string]string{
+					"weakChild": "strongChild",
+				},
+			},
+			&Child{
+				UUID: "weakChild",
+			},
+			&Child{
+				UUID: "strongChild",
+			},
+		},
+		// weak child is deleted
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				MapKeyValueReference: map[string]string{
+					"weakChild": "strongChild",
+				},
+			},
+			&Child{
+				UUID: "strongChild",
+			},
+		},
+		// the reference to weak child should be removed from parent
+		// and strong child should be deleted
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "map_key_value_reference",
+				FromValue:  false,
+			}: database.Reference{
+				"weakChild": []string{"parent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "map_key_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"strongChild": []string{"parent"},
+			},
+		},
+		// neither weak or strong child are referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "map_key_value_reference",
+				FromValue:  false,
+			}: database.Reference{
+				"weakChild": nil,
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "map_key_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"strongChild": nil,
+			},
+		},
+	}
+}
+
+func multipleWeakMapValueReferenceTestData() testData {
+	// when a weak referenced row is deleted, multiple references on a map
+	// value are also deleted
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key1": "child",
+					"key2": "otherChild",
+					"key3": "child",
+				},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// child is deleted
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key1": "child",
+					"key2": "otherChild",
+					"key3": "child",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		// the reference to child should be removed from parent
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+				WeakMapValueReference: map[string]string{
+					"key2": "otherChild",
+				},
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"parent"},
+			},
+		},
+		// child model is no longer referenced, newChild is
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func transitiveStrongReferenceTestData() testData {
+	// when multiple rows are transitively referenced, garbage collection
+	// happens transitively as well
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+			},
+			&Child{
+				UUID:                          "child",
+				StrongAtomicOptionalReference: ptr("grandchild"),
+			},
+			&Grandchild{
+				UUID: "grandchild",
+			},
+		},
+		// parent reference to child is removed
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+			&Child{
+				UUID:                          "child",
+				StrongAtomicOptionalReference: ptr("grandchild"),
+			},
+			&Grandchild{
+				UUID: "grandchild",
+			},
+		},
+		// child and grandchild models should be deleted as it is no longer referenced
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Grandchild",
+				FromTable:  "Child",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"grandchild": []string{"child"},
+			},
+		},
+		// child and grandchild models are no longer referenced
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+			database.ReferenceSpec{
+				ToTable:    "Grandchild",
+				FromTable:  "Child",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"grandchild": nil,
+			},
+		},
+	}
+}
+
+func transitiveStrongAndWeakReferenceTestData() testData {
+	// when a strong referenced is removed, an unreferenced row will be garbage
+	// collected and transitively, weak references to it removed
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+				WeakAtomicOptionalReference:   ptr("child"),
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// parent strong reference to child is removed
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                        "parent",
+				WeakAtomicOptionalReference: ptr("child"),
+			},
+			&Child{
+				UUID: "child",
+			},
+		},
+		// as a result, child and and the weak reference to it is removed
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+		},
+		// child is no longer referenced at all
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func insertNoRootUnreferencedRowTestData() testData {
+	return testData{
+		// new child is inserted
+		updatedModels: []model.Model{
+			&Child{
+				UUID: "newChild",
+			},
+		},
+		// but is removed since is not referenced from anywhere and the table is
+		// not part of the root set
+		finalModels:           nil,
+		wantUpdatedReferences: database.References{},
+	}
+}
+
+func weakReferenceToNonExistentRowTestData() testData {
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		// a weak reference is added no nonexistent row
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                        "parent",
+				WeakAtomicOptionalReference: ptr("child"),
+			},
+		},
+		// but is removed since the row does not exist
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+		},
+	}
+}
+
+func sameRowStrongAndWeakReferenceTestData() testData {
+	// a row needs to have a weak reference garbage collected and
+	// at the same time that row itself is garbage collected due to not
+	// being strongly referenced
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+			},
+			&Child{
+				UUID:                        "child",
+				WeakAtomicOptionalReference: ptr("grandchild"),
+			},
+			&Grandchild{
+				UUID: "grandchild",
+			},
+		},
+		// parent strong reference to child is removed
+		// grand child is removed as well
+		updatedModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+			&Child{
+				UUID:                        "child",
+				WeakAtomicOptionalReference: ptr("grandchild"),
+			},
+		},
+		// as a result, child is removed
+		finalModels: []model.Model{
+			&Parent{
+				UUID: "parent",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Grandchild",
+				FromTable:  "Child",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"grandchild": []string{"child"},
+			},
+		},
+		// neither child nor grandchild are referenced at all
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": nil,
+			},
+			database.ReferenceSpec{
+				ToTable:    "Grandchild",
+				FromTable:  "Child",
+				FromColumn: "weak_atomic_optional_reference",
+			}: database.Reference{
+				"grandchild": nil,
+			},
+		},
+	}
+}
+
+func multipleReferencesTestData() testData {
+	// testing behavior with multiple combinations of references
+	return testData{
+		existingModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongSetReference:            []string{"child"},
+				StrongAtomicOptionalReference: ptr("child"),
+				WeakMapValueReference:         map[string]string{"key1": "yetAnotherChild", "key2": "otherChild"},
+			},
+			&Parent{
+				UUID:                          "otherParent",
+				StrongAtomicOptionalReference: ptr("child"),
+				StrongSetReference:            []string{"otherChild"},
+				WeakSetReference:              []string{"otherChild", "child", "yetAnotherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+			&Child{
+				UUID: "yetAnotherChild",
+			},
+		},
+		// all strong references to child except one are removed
+		// single strong reference to otherChild is removed
+		updatedModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+				WeakMapValueReference:         map[string]string{"key1": "yetAnotherChild", "key2": "otherChild"},
+			},
+			&Parent{
+				UUID:             "otherParent",
+				WeakSetReference: []string{"otherChild", "child", "yetAnotherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "otherChild",
+			},
+			&Child{
+				UUID: "yetAnotherChild",
+			},
+		},
+		// otherChild is garbage collected and all weak references to it removed
+		finalModels: []model.Model{
+			&Parent{
+				UUID:                          "parent",
+				StrongAtomicOptionalReference: ptr("child"),
+				WeakMapValueReference:         map[string]string{"key1": "yetAnotherChild"},
+			},
+			&Parent{
+				UUID:             "otherParent",
+				WeakSetReference: []string{"child", "yetAnotherChild"},
+			},
+			&Child{
+				UUID: "child",
+			},
+			&Child{
+				UUID: "yetAnotherChild",
+			},
+		},
+		existingReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_set_reference",
+			}: database.Reference{
+				"child":      []string{"parent"},
+				"otherChild": []string{"otherParent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent", "otherParent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"yetAnotherChild": []string{"parent"},
+				"otherChild":      []string{"parent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_set_reference",
+			}: database.Reference{
+				"otherChild":      []string{"otherParent"},
+				"child":           []string{"otherParent"},
+				"yetAnotherChild": []string{"otherParent"},
+			},
+		},
+		// all strong references to child except one are removed
+		// all references to otherChild are removed
+		// references to yetAnotherChild are unchanged
+		wantUpdatedReferences: database.References{
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_set_reference",
+			}: database.Reference{
+				"child":      nil,
+				"otherChild": nil,
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "strong_atomic_optional_reference",
+			}: database.Reference{
+				"child": []string{"parent"},
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_map_value_reference",
+				FromValue:  true,
+			}: database.Reference{
+				"otherChild": nil,
+			},
+			database.ReferenceSpec{
+				ToTable:    "Child",
+				FromTable:  "Parent",
+				FromColumn: "weak_set_reference",
+			}: database.Reference{
+				"child":      []string{"otherParent"}, // this reference is read by the reference tracker, but not changed
+				"otherChild": nil,
+			},
+		},
+	}
+}
diff --git a/updates/updates_test.go b/updates/updates_test.go
index 5d318f3..6d67fee 100644
--- a/updates/updates_test.go
+++ b/updates/updates_test.go
@@ -488,9 +488,7 @@ func TestUpdates_AddOperation(t *testing.T) {
 				},
 			},
 			expected: fields{
-				updates: map[string]map[string]modelUpdate{
-					"Bridge": {},
-				},
+				updates: nil,
 			},
 		},
 		{
@@ -1173,9 +1171,7 @@ func TestUpdates_AddOperation(t *testing.T) {
 				},
 			},
 			expected: fields{
-				updates: map[string]map[string]modelUpdate{
-					"Bridge": {},
-				},
+				updates: nil,
 			},
 		},
 		{