From b3434b5746dad3a182019fc7bf9c6e5b4f10bb04 Mon Sep 17 00:00:00 2001 From: Adrian Moreno Date: Mon, 11 Oct 2021 18:24:36 +0200 Subject: [PATCH] mapper: Add compatility mode Allow the user to set a Compatibility flag on the DatabaseModelRequest. When that flag is set, the verification phase will not fail if a column is missing on the schema or has a different type. Instead it will just skip the column. Same goes for missing tables, they will just be skipped. Signed-off-by: Adrian Moreno --- cache/cache.go | 2 +- cmd/stress/stress.go | 1 + mapper/info.go | 31 +++-- mapper/info_test.go | 80 ++++++++++-- mapper/mapper.go | 6 +- mapper/mapper_test.go | 20 +-- model/database.go | 22 +++- model/database_test.go | 288 +++++++++++++++++++++++++++++++++++++++++ model/request.go | 34 +++-- 9 files changed, 434 insertions(+), 50 deletions(-) create mode 100644 model/database_test.go diff --git a/cache/cache.go b/cache/cache.go index c557cd2b..21b7208e 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -797,7 +797,7 @@ func (t *TableCache) ApplyModifications(tableName string, base model.Model, upda return err } for k, v := range update { - if k == "_uuid" { + if k == "_uuid" || !t.dbModel.HasColumn(tableName, k) { continue } diff --git a/cmd/stress/stress.go b/cmd/stress/stress.go index cafafcc9..476f570f 100644 --- a/cmd/stress/stress.go +++ b/cmd/stress/stress.go @@ -250,6 +250,7 @@ func main() { var err error dbModelReq, err = model.NewDatabaseModelRequest("Open_vSwitch", map[string]model.Model{"Open_vSwitch": &ovsType{}, "Bridge": &bridgeType{}}) + dbModelReq.SetCompatibility(true) if err != nil { log.Fatal(err) } diff --git a/mapper/info.go b/mapper/info.go index e364ddd2..c7fbd499 100644 --- a/mapper/info.go +++ b/mapper/info.go @@ -2,6 +2,7 @@ package mapper import ( "fmt" + "log" "reflect" "github.com/ovn-org/libovsdb/ovsdb" @@ -25,13 +26,13 @@ type Metadata struct { func (i *Info) FieldByColumn(column string) (interface{}, error) { fieldName, ok := i.Metadata.Fields[column] if !ok { - return nil, fmt.Errorf("FieldByColumn: column %s not found in orm info", column) + return nil, fmt.Errorf("FieldByColumn: column %s not found in mapper info", column) } return reflect.ValueOf(i.Obj).Elem().FieldByName(fieldName).Interface(), nil } -// FieldByColumn returns the field value that corresponds to a column -func (i *Info) hasColumn(column string) bool { +// HasColumn returns whether the column exists in the Metadata fields +func (i *Info) HasColumn(column string) bool { _, ok := i.Metadata.Fields[column] return ok } @@ -40,7 +41,7 @@ func (i *Info) hasColumn(column string) bool { func (i *Info) SetField(column string, value interface{}) error { fieldName, ok := i.Metadata.Fields[column] if !ok { - return fmt.Errorf("SetField: column %s not found in orm info", column) + return fmt.Errorf("SetField: column %s not found in mapper info", column) } fieldValue := reflect.ValueOf(i.Obj).Elem().FieldByName(fieldName) @@ -64,12 +65,12 @@ func (i *Info) ColumnByPtr(fieldPtr interface{}) (string, error) { if objType.Field(j).Offset == offset { column := objType.Field(j).Tag.Get("ovsdb") if _, ok := i.Metadata.Fields[column]; !ok { - return "", fmt.Errorf("field does not have orm column information") + return "", fmt.Errorf("field does not have mapper column information") } return column, nil } } - return "", fmt.Errorf("field pointer does not correspond to orm struct") + return "", fmt.Errorf("field pointer does not correspond to mapper struct") } // getValidIndexes inspects the object and returns the a list of indexes (set of columns) for witch @@ -85,7 +86,7 @@ func (i *Info) getValidIndexes() ([][]string, error) { OUTER: for _, idx := range possibleIndexes { for _, col := range idx { - if !i.hasColumn(col) { + if !i.HasColumn(col) { continue OUTER } columnSchema := i.Metadata.TableSchema.Column(col) @@ -106,7 +107,7 @@ OUTER: } // NewInfo creates a MapperInfo structure around an object based on a given table schema -func NewInfo(tableName string, table *ovsdb.TableSchema, obj interface{}) (*Info, error) { +func NewInfo(tableName string, table *ovsdb.TableSchema, obj interface{}, compat bool) (*Info, error) { objPtrVal := reflect.ValueOf(obj) if objPtrVal.Type().Kind() != reflect.Ptr { return nil, ovsdb.NewErrWrongType("NewMapperInfo", "pointer to a struct", obj) @@ -127,25 +128,35 @@ func NewInfo(tableName string, table *ovsdb.TableSchema, obj interface{}) (*Info } column := table.Column(colName) if column == nil { - return nil, &ErrMapper{ + err := &ErrMapper{ objType: objType.String(), field: field.Name, fieldType: field.Type.String(), fieldTag: colName, reason: "Column does not exist in schema", } + if compat { + log.Printf("Warning: %s. Skipping", err.Error()) + continue + } + return nil, err } // Perform schema-based type checking expType := ovsdb.NativeType(column) if expType != field.Type { - return nil, &ErrMapper{ + err := &ErrMapper{ objType: objType.String(), field: field.Name, fieldType: field.Type.String(), fieldTag: colName, reason: fmt.Sprintf("Wrong type, column expects %s", expType), } + if compat { + log.Printf("Warning: %s. Skipping", err.Error()) + continue + } + return nil, err } fields[colName] = field.Name } diff --git a/mapper/info_test.go b/mapper/info_test.go index cf5ad960..7771c473 100644 --- a/mapper/info_test.go +++ b/mapper/info_test.go @@ -39,17 +39,77 @@ func TestNewMapperInfo(t *testing.T) { table []byte obj interface{} expectedCols []string + compat bool err bool } tests := []test{ { - name: "no_orm", + name: "Info from object without tags should return no columns", table: sampleTable, obj: &struct { foo string bar int }{}, - err: false, + expectedCols: []string{}, + compat: false, + err: false, + }, + { + name: "Valid model should contain columns", + table: sampleTable, + obj: &struct { + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + }{}, + expectedCols: []string{"aString", "aInteger"}, + compat: true, + err: false, + }, + { + name: "Extra column no compat should error", + table: sampleTable, + obj: &struct { + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz int `ovsdb:"aNonExistingCol"` + }{}, + expectedCols: []string{"aString", "aInteger"}, + compat: false, + err: true, + }, + { + name: "Extra column compat should not error", + table: sampleTable, + obj: &struct { + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz int `ovsdb:"aNonExistingCol"` + }{}, + expectedCols: []string{"aString", "aInteger"}, + compat: true, + err: false, + }, + { + name: "Different column typ no compat should error", + table: sampleTable, + obj: &struct { + Foo string `ovsdb:"aString"` + Bar string `ovsdb:"aInt"` + }{}, + expectedCols: []string{"aString", "aInt"}, + compat: false, + err: true, + }, + { + name: "Different column type compat should not error", + table: sampleTable, + obj: &struct { + Foo string `ovsdb:"aString"` + Bar string `ovsdb:"aInt"` + }{}, + expectedCols: []string{"aString"}, + compat: true, + err: false, }, } for _, tt := range tests { @@ -58,16 +118,16 @@ func TestNewMapperInfo(t *testing.T) { err := json.Unmarshal(tt.table, &table) assert.Nil(t, err) - info, err := NewInfo("Test", &table, tt.obj) + info, err := NewInfo("Test", &table, tt.obj, tt.compat) if tt.err { assert.NotNil(t, err) } else { assert.Nil(t, err) + for _, col := range tt.expectedCols { + assert.Truef(t, info.HasColumn(col), "Expected column %s should be present in Mapper Info", col) + } + assert.Equal(t, "Test", info.Metadata.TableName) } - for _, col := range tt.expectedCols { - assert.Truef(t, info.hasColumn(col), "Expected column should be present in Mapper Info") - } - assert.Equal(t, "Test", info.Metadata.TableName) }) } @@ -142,7 +202,7 @@ func TestMapperInfoSet(t *testing.T) { err := json.Unmarshal(tt.table, &table) assert.Nil(t, err) - info, err := NewInfo("Test", &table, tt.obj) + info, err := NewInfo("Test", &table, tt.obj, false) assert.Nil(t, err) err = info.SetField(tt.column, tt.field) @@ -223,7 +283,7 @@ func TestMapperInfoColByPtr(t *testing.T) { err := json.Unmarshal(tt.table, &table) assert.Nil(t, err) - info, err := NewInfo("Test", &table, tt.obj) + info, err := NewInfo("Test", &table, tt.obj, false) assert.Nil(t, err) col, err := info.ColumnByPtr(tt.field) @@ -355,7 +415,7 @@ func TestOrmGetIndex(t *testing.T) { } for _, tt := range tests { t.Run(fmt.Sprintf("GetValidIndexes_%s", tt.name), func(t *testing.T) { - info, err := NewInfo("Test", &table, tt.obj) + info, err := NewInfo("Test", &table, tt.obj, false) assert.Nil(t, err) indexes, err := info.getValidIndexes() diff --git a/mapper/mapper.go b/mapper/mapper.go index ae01e4b2..97fc017f 100644 --- a/mapper/mapper.go +++ b/mapper/mapper.go @@ -72,7 +72,7 @@ func (m Mapper) GetRowData(row *ovsdb.Row, result *Info) error { // The result object must be given as pointer to an object with the right tags func (m Mapper) getData(ovsData ovsdb.Row, result *Info) error { for name, column := range result.Metadata.TableSchema.Columns { - if !result.hasColumn(name) { + if !result.HasColumn(name) { // If provided struct does not have a field to hold this value, skip it continue } @@ -243,7 +243,7 @@ func (m Mapper) NewCondition(data *Info, field interface{}, function ovsdb.Condi // It takes care of field validation against the column type func (m Mapper) NewMutation(data *Info, column string, mutator ovsdb.Mutator, value interface{}) (*ovsdb.Mutation, error) { // Check the column exists in the object - if !data.hasColumn(column) { + if !data.HasColumn(column) { return nil, fmt.Errorf("mutation contains column %s that does not exist in object %v", column, data) } // Check that the mutation is valid @@ -304,7 +304,7 @@ func (m Mapper) equalIndexes(one, other *Info, indexes ...string) (bool, error) if reflect.DeepEqual(ridx, lidx) { // All columns in an index must be simultaneously equal for _, col := range lidx { - if !one.hasColumn(col) || !other.hasColumn(col) { + if !one.HasColumn(col) || !other.HasColumn(col) { break } lfield, err := one.FieldByColumn(col) diff --git a/mapper/mapper_test.go b/mapper/mapper_test.go index 7cb3aa80..9e304060 100644 --- a/mapper/mapper_test.go +++ b/mapper/mapper_test.go @@ -226,7 +226,7 @@ func TestMapperGetData(t *testing.T) { test := ormTestType{ NonTagged: "something", } - testInfo, err := NewInfo("TestTable", schema.Table("TestTable"), &test) + testInfo, err := NewInfo("TestTable", schema.Table("TestTable"), &test, false) assert.NoError(t, err) err = mapper.GetRowData(&ovsRow, testInfo) @@ -345,7 +345,7 @@ func TestMapperNewRow(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("NewRow: %s", test.name), func(t *testing.T) { mapper := NewMapper(&schema) - info, err := NewInfo("TestTable", schema.Table("TestTable"), test.objInput) + info, err := NewInfo("TestTable", schema.Table("TestTable"), test.objInput, false) assert.NoError(t, err) row, err := mapper.NewRow(info) if test.shoulderr { @@ -438,7 +438,7 @@ func TestMapperNewRowFields(t *testing.T) { testObj.MyFloat = 0 test.prepare(&testObj) - info, err := NewInfo("TestTable", schema.Table("TestTable"), &testObj) + info, err := NewInfo("TestTable", schema.Table("TestTable"), &testObj, false) assert.NoError(t, err) row, err := mapper.NewRow(info, test.fields...) if test.err { @@ -592,7 +592,7 @@ func TestMapperCondition(t *testing.T) { for _, tt := range tests { t.Run(fmt.Sprintf("newEqualityCondition_%s", tt.name), func(t *testing.T) { tt.prepare(&testObj) - info, err := NewInfo("TestTable", schema.Table("TestTable"), &testObj) + info, err := NewInfo("TestTable", schema.Table("TestTable"), &testObj, false) assert.NoError(t, err) conds, err := mapper.NewEqualityCondition(info, tt.index...) @@ -846,9 +846,9 @@ func TestMapperEqualIndexes(t *testing.T) { } for _, test := range tests { t.Run(fmt.Sprintf("Equal %s", test.name), func(t *testing.T) { - info1, err := NewInfo("TestTable", schema.Table("TestTable"), &test.obj1) + info1, err := NewInfo("TestTable", schema.Table("TestTable"), &test.obj1, false) assert.NoError(t, err) - info2, err := NewInfo("TestTable", schema.Table("TestTable"), &test.obj2) + info2, err := NewInfo("TestTable", schema.Table("TestTable"), &test.obj2, false) assert.NoError(t, err) eq, err := mapper.equalIndexes(info1, info2, test.indexes...) assert.Nil(t, err) @@ -873,9 +873,9 @@ func TestMapperEqualIndexes(t *testing.T) { Int1: 42, Int2: 25, } - info1, err := NewInfo("TestTable", schema.Table("TestTable"), &obj1) + info1, err := NewInfo("TestTable", schema.Table("TestTable"), &obj1, false) assert.NoError(t, err) - info2, err := NewInfo("TestTable", schema.Table("TestTable"), &obj2) + info2, err := NewInfo("TestTable", schema.Table("TestTable"), &obj2, false) assert.NoError(t, err) eq, err := mapper.EqualFields(info1, info2, &obj1.Int1, &obj1.Int2) assert.Nil(t, err) @@ -1031,7 +1031,7 @@ func TestMapperMutation(t *testing.T) { } for _, test := range tests { t.Run(fmt.Sprintf("newMutation%s", test.name), func(t *testing.T) { - info, err := NewInfo("TestTable", schema.Table("TestTable"), &test.obj) + info, err := NewInfo("TestTable", schema.Table("TestTable"), &test.obj, false) assert.NoError(t, err) mutation, err := mapper.NewMutation(info, test.column, test.mutator, test.value) @@ -1119,7 +1119,7 @@ func TestNewMonitorRequest(t *testing.T) { require.NoError(t, err) mapper := NewMapper(&schema) testTable := &testType{} - info, err := NewInfo("TestTable", schema.Table("TestTable"), testTable) + info, err := NewInfo("TestTable", schema.Table("TestTable"), testTable, false) assert.NoError(t, err) mr, err := mapper.NewMonitorRequest(info, nil) require.NoError(t, err) diff --git a/model/database.go b/model/database.go index a19e8b41..7e15ffd3 100644 --- a/model/database.go +++ b/model/database.go @@ -15,7 +15,7 @@ type DatabaseModel struct { request *DatabaseModelRequest schema *ovsdb.DatabaseSchema mapper *mapper.Mapper - metadata map[reflect.Type]*mapper.Metadata + metadata map[string]*mapper.Metadata } // NewDatabaseModel returns a new DatabaseModel @@ -111,11 +111,10 @@ func (db *DatabaseModel) FindTable(mType reflect.Type) string { // database and caches them for future re-use func (db *DatabaseModel) generateModelInfo() []error { errors := []error{} - metadata := make(map[reflect.Type]*mapper.Metadata, len(db.request.types)) - for tableName, tType := range db.request.types { + metadata := make(map[string]*mapper.Metadata, len(db.request.types)) + for tableName := range db.request.types { tableSchema := db.schema.Table(tableName) if tableSchema == nil { - errors = append(errors, fmt.Errorf("Database Model contains model for table %s which is not present in schema", tableName)) continue } obj, err := db.NewModel(tableName) @@ -123,12 +122,12 @@ func (db *DatabaseModel) generateModelInfo() []error { errors = append(errors, err) continue } - info, err := mapper.NewInfo(tableName, tableSchema, obj) + info, err := mapper.NewInfo(tableName, tableSchema, obj, db.request.compat) if err != nil { errors = append(errors, err) continue } - metadata[tType] = info.Metadata + metadata[tableName] = info.Metadata } db.metadata = metadata return errors @@ -136,7 +135,7 @@ func (db *DatabaseModel) generateModelInfo() []error { // NewModelInfo returns a mapper.Info object based on a provided model func (db *DatabaseModel) NewModelInfo(obj interface{}) (*mapper.Info, error) { - meta, ok := db.metadata[reflect.TypeOf(obj)] + meta, ok := db.metadata[db.FindTable(reflect.TypeOf(obj))] if !ok { return nil, ovsdb.NewErrWrongType("NewModelInfo", "type that is part of the DatabaseModel", obj) } @@ -145,3 +144,12 @@ func (db *DatabaseModel) NewModelInfo(obj interface{}) (*mapper.Info, error) { Metadata: meta, }, nil } + +func (db *DatabaseModel) HasColumn(tableName, column string) bool { + meta, ok := db.metadata[tableName] + if !ok { + return false + } + _, ok = meta.Fields[column] + return ok +} diff --git a/model/database_test.go b/model/database_test.go new file mode 100644 index 00000000..8f377146 --- /dev/null +++ b/model/database_test.go @@ -0,0 +1,288 @@ +package model + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ovn-org/libovsdb/ovsdb" + "github.com/stretchr/testify/require" +) + +var tableA = ` + "TableA": { + "columns": { + "aString": { + "type": "string" + }, + "aInteger": { + "type": "integer" + }, + "aSet": { + "type": { + "key": "string", + "max": "unlimited", + "min": 0 + } + } + } +}` +var tableB = ` + "TableB": { + "columns": { + "aString": { + "type": "string" + }, + "aInteger": { + "type": "integer" + }, + "aSet": { + "type": { + "key": "string", + "max": "unlimited", + "min": 0 + } + } + } +}` + +var schema = ` { + "cksum": "223619766 22548", + "name": "TestSchema", + "tables": {` + tableA + "," + tableB + ` + } + } +` + +func TestNewDatabaseModel(t *testing.T) { + + tests := []struct { + name string + schema string + requestTypes map[string]Model + compat bool + expectedCols map[string][]string + expectedNotCols map[string][]string + err bool + }{ + { + name: "Fully matching model should succeed", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + "TableB": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + expectedCols: map[string][]string{ + "TableA": {"aString", "aInteger", "aSet"}, + "TableB": {"aString", "aInteger", "aSet"}, + }, + compat: false, + err: false, + }, + { + name: "Model with less tables should succeed", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + expectedCols: map[string][]string{ + "TableA": {"aString", "aInteger", "aSet"}, + }, + compat: false, + err: false, + }, + { + name: "Model with less tables should succeed", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + expectedCols: map[string][]string{ + "TableA": {"aString", "aInteger", "aSet"}, + }, + compat: false, + err: false, + }, + { + name: "Model more tables should fail", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + "TableB": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + "TableC": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + compat: false, + err: true, + }, + { + name: "Model with more tables (compat) should succeed", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + "TableB": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + "TableC": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + expectedCols: map[string][]string{ + "TableA": {"aString", "aInteger", "aSet"}, + "TableB": {"aString", "aInteger", "aSet"}, + }, + expectedNotCols: map[string][]string{ + "TableC": {"aString", "aInteger", "aSet"}, + }, + compat: true, + err: false, + }, + { + name: "Model with more columns should fail", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + FooBar []string `ovsdb:"aSecondSet"` + }{}, + "TableB": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + compat: false, + err: true, + }, + { + name: "Model with more columns (compat) should succeed", + schema: schema, + requestTypes: map[string]Model{ + "TableA": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + FooBar []string `ovsdb:"aSecondSet"` + }{}, + "TableB": &struct { + UUID string `ovsdb:"_uuid"` + Foo string `ovsdb:"aString"` + Bar int `ovsdb:"aInteger"` + Baz []string `ovsdb:"aSet"` + }{}, + }, + expectedCols: map[string][]string{ + "TableA": {"aString", "aInteger", "aSet"}, + "TableB": {"aString", "aInteger", "aSet"}, + }, + expectedNotCols: map[string][]string{ + "TableA": {"aSecondSet"}, + }, + compat: false, + err: true, + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("NewDatabaseModel%s", tt.name), func(t *testing.T) { + var schema ovsdb.DatabaseSchema + err := json.Unmarshal([]byte(tt.schema), &schema) + require.NoError(t, err) + req, err := NewDatabaseModelRequest("TestSchema", tt.requestTypes) + if tt.compat { + req.SetCompatibility(true) + } + require.NoError(t, err) + + // Test single step creation + db, errs := NewDatabaseModel(&schema, req) + + if tt.err { + require.NotEmpty(t, errs) + } else { + require.Empty(t, errs) + for table, cols := range tt.expectedCols { + for _, col := range cols { + require.Truef(t, db.HasColumn(table, col), "table %s column %s should be present in model", table, col) + } + } + for table, cols := range tt.expectedNotCols { + for _, col := range cols { + require.Falsef(t, db.HasColumn(table, col), "table %s column %s should not be present in model", table, col) + } + } + } + + // Test 2-step step creation + db = NewPartialDatabaseModel(req) + errs = db.SetSchema(&schema) + + if tt.err { + require.NotEmpty(t, errs) + } else { + require.Empty(t, errs) + for table, cols := range tt.expectedCols { + for _, col := range cols { + require.Truef(t, db.HasColumn(table, col), "table %s column %s should be present in model", table, col) + } + } + for table, cols := range tt.expectedNotCols { + for _, col := range cols { + require.Falsef(t, db.HasColumn(table, col), "table %s column %s should not be present in model", table, col) + } + } + } + + }) + } + +} diff --git a/model/request.go b/model/request.go index e8b82501..c62fb74d 100644 --- a/model/request.go +++ b/model/request.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "log" "reflect" "github.com/ovn-org/libovsdb/mapper" @@ -10,12 +11,13 @@ import ( // DatabaseModelRequest contains the information needed to build a DatabaseModel type DatabaseModelRequest struct { - name string - types map[string]reflect.Type + name string + types map[string]reflect.Type + compat bool } // NewModel returns a new instance of a model from a specific string -func (db DatabaseModelRequest) newModel(table string) (Model, error) { +func (db *DatabaseModelRequest) newModel(table string) (Model, error) { mtype, ok := db.types[table] if !ok { return nil, fmt.Errorf("table %s not found in database model", string(table)) @@ -24,14 +26,21 @@ func (db DatabaseModelRequest) newModel(table string) (Model, error) { return model.Interface().(Model), nil } +// SetCompatible requests the DatabaseModel to try to be compatible with the server schema. +// On this mode, the model will try to fill in fields if they exist on the database but will not fail +// if additional columns or tables are declared in the model. +func (db *DatabaseModelRequest) SetCompatibility(compat bool) { + db.compat = compat +} + // Name returns the database name -func (db DatabaseModelRequest) Name() string { +func (db *DatabaseModelRequest) Name() string { return db.name } // Validate validates the DatabaseModel against the input schema // Returns all the errors detected -func (db DatabaseModelRequest) validate(schema *ovsdb.DatabaseSchema) []error { +func (db *DatabaseModelRequest) validate(schema *ovsdb.DatabaseSchema) []error { var errors []error if db.name != schema.Name { errors = append(errors, fmt.Errorf("database model name (%s) does not match schema (%s)", @@ -41,7 +50,12 @@ func (db DatabaseModelRequest) validate(schema *ovsdb.DatabaseSchema) []error { for tableName := range db.types { tableSchema := schema.Table(tableName) if tableSchema == nil { - errors = append(errors, fmt.Errorf("database model contains a model for table %s that does not exist in schema", tableName)) + err := fmt.Errorf("database model contains a model for table %s that does not exist in schema", tableName) + if db.compat { + log.Printf("Warning: %s. Skipping", err.Error()) + } else { + errors = append(errors, err) + } continue } model, err := db.newModel(tableName) @@ -49,7 +63,8 @@ func (db DatabaseModelRequest) validate(schema *ovsdb.DatabaseSchema) []error { errors = append(errors, err) continue } - if _, err := mapper.NewInfo(tableName, tableSchema, model); err != nil { + _, err = mapper.NewInfo(tableName, tableSchema, model, db.compat) + if err != nil { errors = append(errors, err) } } @@ -78,7 +93,8 @@ func NewDatabaseModelRequest(name string, models map[string]Model) (*DatabaseMod types[table] = reflect.TypeOf(model) } return &DatabaseModelRequest{ - types: types, - name: name, + types: types, + name: name, + compat: false, }, nil }