From 05ccc72a6b868877b90acf57ca3f0bfcbf390984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= Date: Sat, 22 Jul 2017 18:22:08 -0400 Subject: [PATCH 1/5] db: draft dirty entities --- db.go | 2 + internal/sqladapter/testing/adapter.go.tpl | 28 ++++ upper/entity.go | 59 ++++++++ upper/entity_test.go | 150 +++++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 upper/entity.go create mode 100644 upper/entity_test.go diff --git a/db.go b/db.go index b8ec8bf5..80435018 100644 --- a/db.go +++ b/db.go @@ -172,6 +172,8 @@ type Unmarshaler interface { UnmarshalDB(interface{}) error } +type Changeset map[string]interface{} + // Cond is a map that defines conditions for a query and satisfies the // Constraints and Compound interfaces. // diff --git a/internal/sqladapter/testing/adapter.go.tpl b/internal/sqladapter/testing/adapter.go.tpl index 76a21d19..eaafbb24 100644 --- a/internal/sqladapter/testing/adapter.go.tpl +++ b/internal/sqladapter/testing/adapter.go.tpl @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" + "upper.io/db.v3/upper" ) type customLogger struct { @@ -1750,3 +1751,30 @@ func TestCustomType(t *testing.T) { assert.Equal(t, "foo: some name", string(bar.Custom.Val)) } + +func TestDirtyEntity(t *testing.T) { + type entityTest struct { + Artist artistType `db:",inline"` + + db.Model + } + var item entityTest + item.Name = "Hello" + + sess := mustOpen() + + artist := sess.Collection("artist") + + err := artist.Truncate() + assert.NoError(t, err) + + id, err := artist.InsertReturning(newArtist) + assert.NoError(t, err) + assert.NotNil(t, id) + + var bar artistWithCustomType + err = artist.Find(id).One(&bar) + assert.NoError(t, err) + + assert.Equal(t, "foo: some name", string(bar.Custom.Val)) +} diff --git a/upper/entity.go b/upper/entity.go new file mode 100644 index 00000000..54eb2626 --- /dev/null +++ b/upper/entity.go @@ -0,0 +1,59 @@ +package repo + +import ( + "sync" + "upper.io/db.v3" + "upper.io/db.v3/lib/sqlbuilder" +) + +type Mapper interface { + Store(interface{}) error + Changeset() (db.Changeset, error) +} + +type Entity struct { + initialValues db.Changeset + ref interface{} + mu sync.RWMutex +} + +var _ = Mapper(&Entity{}) + +func (e *Entity) Changeset() (db.Changeset, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + cols, vals, err := sqlbuilder.Map(e.ref, nil) + if err != nil { + return nil, err + } + + var changeset db.Changeset + for i := range vals { + if vals[i] == e.initialValues[cols[i]] { + continue + } + if changeset == nil { + changeset = make(db.Changeset) + } + changeset[cols[i]] = vals[i] + } + return changeset, nil +} + +func (e *Entity) Store(v interface{}) error { + cols, vals, err := sqlbuilder.Map(v, nil) + if err != nil { + return err + } + + e.mu.Lock() + e.initialValues = make(db.Changeset) + for i := range cols { + e.initialValues[cols[i]] = vals[i] + } + e.ref = v + e.mu.Unlock() + + return nil +} diff --git a/upper/entity_test.go b/upper/entity_test.go new file mode 100644 index 00000000..e034e8da --- /dev/null +++ b/upper/entity_test.go @@ -0,0 +1,150 @@ +package repo + +import ( + "fmt" + "reflect" + "testing" + + "upper.io/db.v3" +) + +type testStruct struct { + ID int `db:"id,omitempty"` + StringValue string `db:"string_value"` + IntValue int `db:"int_value"` + BoolValue bool `db:"bool_value"` + FloatValue float64 `db:"float_value"` + + PointerToFloatValue *float64 `db:"ptr_float_value"` + PointerToStringValue *string `db:"ptr_string_value"` + + Entity +} + +var ( + stringValue = "hello world!" + floatValue = 5.555 +) + +var testCases = []struct { + in Mapper + fn func(interface{}) + out db.Changeset +}{ + { + &testStruct{ + ID: 1, + StringValue: "five", + BoolValue: false, + IntValue: 4, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 2 + u.BoolValue = false + }, + db.Changeset{ + "id": 2, + }, + }, + { + &testStruct{ + ID: 1, + StringValue: "five", + BoolValue: false, + IntValue: 4, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 2 + u.StringValue = "four" + u.FloatValue = 0 + u.BoolValue = false + }, + db.Changeset{ + "id": 2, + "string_value": "four", + }, + }, + { + &testStruct{ + ID: 1, + StringValue: "five", + BoolValue: false, + IntValue: 4, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 2 + u.StringValue = "four" + u.FloatValue = 1.23 + u.BoolValue = false + }, + db.Changeset{ + "id": 2, + "string_value": "four", + "float_value": 1.23, + }, + }, + { + &testStruct{}, + func(update interface{}) { + u := update.(*testStruct) + u.PointerToStringValue = &stringValue + }, + db.Changeset{ + "ptr_string_value": &stringValue, + }, + }, + { + &testStruct{ + PointerToStringValue: &stringValue, + }, + func(update interface{}) { + u := update.(*testStruct) + u.PointerToStringValue = &stringValue + }, + nil, + }, + { + &testStruct{ + FloatValue: 2.123, + PointerToFloatValue: &floatValue, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 9 + }, + db.Changeset{ + "id": 9, + }, + }, + { + &testStruct{}, + nil, + nil, + }, +} + +func TestChangeset(t *testing.T) { + for i := range testCases { + + s := testCases[i].in + if err := s.Store(s); err != nil { + t.Fatal(err) + } + + if fn := testCases[i].fn; fn != nil { + fn(s) + } + + values, err := s.Changeset() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(testCases[i].out, values) { + t.Fatal(fmt.Sprintf("test: %v, expected: %#v, got: %#v", i, testCases[i].out, values)) + } + } +} From acf10d29ea8d266402b8da2882f11e9dffca2825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= Date: Sun, 30 Jul 2017 12:38:47 +0000 Subject: [PATCH 2/5] postgresql: add test for dirty entity --- internal/sqladapter/testing/adapter.go.tpl | 19 ++++++------- {upper => lib/sqlbuilder}/entity.go | 7 ++--- {upper => lib/sqlbuilder}/entity_test.go | 33 +++++++++++++++++++++- 3 files changed, 43 insertions(+), 16 deletions(-) rename {upper => lib/sqlbuilder}/entity.go (86%) rename {upper => lib/sqlbuilder}/entity_test.go (84%) diff --git a/internal/sqladapter/testing/adapter.go.tpl b/internal/sqladapter/testing/adapter.go.tpl index eaafbb24..0f58fdb0 100644 --- a/internal/sqladapter/testing/adapter.go.tpl +++ b/internal/sqladapter/testing/adapter.go.tpl @@ -18,7 +18,6 @@ import ( "github.com/stretchr/testify/assert" "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" - "upper.io/db.v3/upper" ) type customLogger struct { @@ -1754,12 +1753,10 @@ func TestCustomType(t *testing.T) { func TestDirtyEntity(t *testing.T) { type entityTest struct { - Artist artistType `db:",inline"` + artistType `db:",inline"` - db.Model + sqlbuilder.Entity } - var item entityTest - item.Name = "Hello" sess := mustOpen() @@ -1768,13 +1765,13 @@ func TestDirtyEntity(t *testing.T) { err := artist.Truncate() assert.NoError(t, err) - id, err := artist.InsertReturning(newArtist) + var newArtist entityTest + newArtist.Name = "Celia" + err = artist.InsertReturning(&newArtist) assert.NoError(t, err) - assert.NotNil(t, id) + assert.NotZero(t, newArtist.ID) - var bar artistWithCustomType - err = artist.Find(id).One(&bar) + newArtist.Name = "Another name" + err = artist.UpdateReturning(&newArtist) assert.NoError(t, err) - - assert.Equal(t, "foo: some name", string(bar.Custom.Val)) } diff --git a/upper/entity.go b/lib/sqlbuilder/entity.go similarity index 86% rename from upper/entity.go rename to lib/sqlbuilder/entity.go index 54eb2626..c81216fa 100644 --- a/upper/entity.go +++ b/lib/sqlbuilder/entity.go @@ -1,9 +1,8 @@ -package repo +package sqlbuilder import ( "sync" "upper.io/db.v3" - "upper.io/db.v3/lib/sqlbuilder" ) type Mapper interface { @@ -23,7 +22,7 @@ func (e *Entity) Changeset() (db.Changeset, error) { e.mu.RLock() defer e.mu.RUnlock() - cols, vals, err := sqlbuilder.Map(e.ref, nil) + cols, vals, err := Map(e.ref, nil) if err != nil { return nil, err } @@ -42,7 +41,7 @@ func (e *Entity) Changeset() (db.Changeset, error) { } func (e *Entity) Store(v interface{}) error { - cols, vals, err := sqlbuilder.Map(v, nil) + cols, vals, err := Map(v, nil) if err != nil { return err } diff --git a/upper/entity_test.go b/lib/sqlbuilder/entity_test.go similarity index 84% rename from upper/entity_test.go rename to lib/sqlbuilder/entity_test.go index e034e8da..a495758e 100644 --- a/upper/entity_test.go +++ b/lib/sqlbuilder/entity_test.go @@ -1,4 +1,4 @@ -package repo +package sqlbuilder import ( "fmt" @@ -124,6 +124,37 @@ var testCases = []struct { nil, nil, }, + { + &testStruct{ + FloatValue: 6.6, + }, + nil, + nil, + }, + { + &testStruct{ + FloatValue: 6.6, + }, + func(update interface{}) { + u := update.(*testStruct) + u.FloatValue = 0 + }, + db.Changeset{ + "float_value": float64(0), + }, + }, + { + &testStruct{ + PointerToStringValue: &stringValue, + }, + func(update interface{}) { + u := update.(*testStruct) + u.PointerToStringValue = nil + }, + db.Changeset{ + "ptr_string_value": nil, + }, + }, } func TestChangeset(t *testing.T) { From 82ed15ab69f752da454422f5cafdb7ec68b4beff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= Date: Sun, 30 Jul 2017 18:22:53 +0000 Subject: [PATCH 3/5] db: add tests for dirty fields --- internal/sqladapter/collection.go | 21 ++ internal/sqladapter/testing/adapter.go.tpl | 53 +++- lib/sqlbuilder/builder.go | 34 ++- lib/sqlbuilder/entity.go | 20 +- lib/sqlbuilder/entity_test.go | 313 +++++++++++---------- lib/sqlbuilder/mapper_test.go | 206 ++++++++++++++ postgresql/custom_types_test.go | 3 +- postgresql/local_test.go | 10 + 8 files changed, 498 insertions(+), 162 deletions(-) create mode 100644 lib/sqlbuilder/mapper_test.go diff --git a/internal/sqladapter/collection.go b/internal/sqladapter/collection.go index f5aef2e5..933beafa 100644 --- a/internal/sqladapter/collection.go +++ b/internal/sqladapter/collection.go @@ -8,6 +8,7 @@ import ( "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter/exql" "upper.io/db.v3/lib/reflectx" + "upper.io/db.v3/lib/sqlbuilder" ) var mapper = reflectx.NewMapper("db") @@ -158,6 +159,10 @@ func (c *collection) InsertReturning(item interface{}) error { col := tx.(Database).Collection(c.Name()) + if mapper, ok := item.(sqlbuilder.Mapper); ok { + _ = mapper.Store(nil) + } + // Insert item as is and grab the returning ID. id, err := col.Insert(item) if err != nil { @@ -195,6 +200,10 @@ func (c *collection) InsertReturning(item interface{}) error { goto cancel } + if mapper, ok := item.(sqlbuilder.Mapper); ok { + _ = mapper.Store(item) + } + if !inTx { // This is only executed if t.Database() was **not** a transaction and if // sess was created with sess.NewTransaction(). @@ -220,6 +229,14 @@ func (c *collection) UpdateReturning(item interface{}) error { return fmt.Errorf("Expecting a pointer but got %T", item) } + if mapper, ok := item.(sqlbuilder.Mapper); ok { + values, err := mapper.Changeset() + if err == nil && len(values) == 0 { + // No operation. + return nil + } + } + // Grab primary keys pks := c.PrimaryKeys() if len(pks) == 0 { @@ -287,6 +304,10 @@ func (c *collection) UpdateReturning(item interface{}) error { panic("default") } + if mapper, ok := item.(sqlbuilder.Mapper); ok { + mapper.Store(item) + } + if !inTx { // This is only executed if t.Database() was **not** a transaction and if // sess was created with sess.NewTransaction(). diff --git a/internal/sqladapter/testing/adapter.go.tpl b/internal/sqladapter/testing/adapter.go.tpl index 03ed2055..6985c680 100644 --- a/internal/sqladapter/testing/adapter.go.tpl +++ b/internal/sqladapter/testing/adapter.go.tpl @@ -1969,6 +1969,7 @@ func TestDirtyEntity(t *testing.T) { } sess := mustOpen() + defer sess.Close() artist := sess.Collection("artist") @@ -1977,11 +1978,55 @@ func TestDirtyEntity(t *testing.T) { var newArtist entityTest newArtist.Name = "Celia" - err = artist.InsertReturning(&newArtist) + + for i := 0; i < 5; i++ { + err = artist.InsertReturning(&newArtist) + if i == 0 { + assert.NoError(t, err) + assert.NotZero(t, newArtist.ID) + } else { + assert.Error(t, err) + } + } + + var newArtist2 entityTest + newArtist2.Name = "MarĂ­a" + err = artist.InsertReturning(&newArtist2) + assert.NoError(t, err) + assert.NotZero(t, newArtist2.ID) + + for i := 0; i < 5; i++ { + newArtist2.Name = "Another name" + err = artist.UpdateReturning(&newArtist2) + assert.NoError(t, err) + } + + var newArtist3 entityTest + artist.Find().One(&newArtist3) + + err = artist.UpdateReturning(&newArtist3) assert.NoError(t, err) - assert.NotZero(t, newArtist.ID) - newArtist.Name = "Another name" - err = artist.UpdateReturning(&newArtist) + newArtist3.Name = "Waka" + err = artist.UpdateReturning(&newArtist3) assert.NoError(t, err) + + for i := 0; i < 5; i++ { + newArtist3.Name = fmt.Sprintf("Artist %d", i) + err = artist.UpdateReturning(&newArtist3) + assert.NoError(t, err) + } + + newArtist4 := entityTest{} + newArtist4.Name = "Alex" + for i := 0; i < 5; i++ { + id, err := artist.Insert(&newArtist4) + assert.NoError(t, err) + + err = artist.Find(id).Update(&newArtist4) + assert.NoError(t, err) + + err = artist.Find(id).Update(&newArtist4) + assert.NoError(t, err) + } } diff --git a/lib/sqlbuilder/builder.go b/lib/sqlbuilder/builder.go index 058357da..4b5cb42c 100644 --- a/lib/sqlbuilder/builder.go +++ b/lib/sqlbuilder/builder.go @@ -27,7 +27,6 @@ import ( "database/sql" "errors" "fmt" - "log" "reflect" "regexp" "sort" @@ -242,6 +241,33 @@ func (b *sqlBuilder) Update(table string) Updater { // Map receives a pointer to map or struct and maps it to columns and values. func Map(item interface{}, options *MapOptions) ([]string, []interface{}, error) { + + if mapper, ok := item.(Mapper); ok { + changeset, err := mapper.changesetWithOptions(options) + if err != nil { + if err == ErrMapperNotInitialized { + return doMap(item, options) + } + return nil, nil, err + } + + var fv fieldValue + fv.values = make([]interface{}, 0, len(changeset)) + fv.fields = make([]string, 0, len(changeset)) + + for field := range changeset { + fv.fields = append(fv.fields, field) + fv.values = append(fv.values, changeset[field]) + } + + sort.Sort(&fv) + return fv.fields, fv.values, nil + } + + return doMap(item, options) +} + +func doMap(item interface{}, options *MapOptions) ([]string, []interface{}, error) { var fv fieldValue if options == nil { options = &defaultMapOptions @@ -527,6 +553,11 @@ func (iter *iterator) next(dst ...interface{}) error { defer iter.Close() return err } + if mapper, ok := dst[0].(Mapper); ok { + if err := mapper.Store(dst[0]); err != nil { + return err + } + } return nil } @@ -574,7 +605,6 @@ func newSqlgenProxy(db *sql.DB, t *exql.Template) *exprProxy { } func (p *exprProxy) Context() context.Context { - log.Printf("Missing context") return context.Background() } diff --git a/lib/sqlbuilder/entity.go b/lib/sqlbuilder/entity.go index c81216fa..8beb951f 100644 --- a/lib/sqlbuilder/entity.go +++ b/lib/sqlbuilder/entity.go @@ -1,13 +1,19 @@ package sqlbuilder import ( + "errors" "sync" + "upper.io/db.v3" ) +var ErrMapperNotInitialized = errors.New("Mapper not initialized") + type Mapper interface { Store(interface{}) error Changeset() (db.Changeset, error) + + changesetWithOptions(options *MapOptions) (db.Changeset, error) } type Entity struct { @@ -18,11 +24,15 @@ type Entity struct { var _ = Mapper(&Entity{}) -func (e *Entity) Changeset() (db.Changeset, error) { +func (e *Entity) changesetWithOptions(options *MapOptions) (db.Changeset, error) { e.mu.RLock() defer e.mu.RUnlock() - cols, vals, err := Map(e.ref, nil) + if e.ref == nil { + return nil, ErrMapperNotInitialized + } + + cols, vals, err := doMap(e.ref, options) if err != nil { return nil, err } @@ -40,8 +50,12 @@ func (e *Entity) Changeset() (db.Changeset, error) { return changeset, nil } +func (e *Entity) Changeset() (db.Changeset, error) { + return e.changesetWithOptions(nil) +} + func (e *Entity) Store(v interface{}) error { - cols, vals, err := Map(v, nil) + cols, vals, err := doMap(v, nil) if err != nil { return err } diff --git a/lib/sqlbuilder/entity_test.go b/lib/sqlbuilder/entity_test.go index a495758e..5905b9cf 100644 --- a/lib/sqlbuilder/entity_test.go +++ b/lib/sqlbuilder/entity_test.go @@ -8,159 +8,166 @@ import ( "upper.io/db.v3" ) -type testStruct struct { - ID int `db:"id,omitempty"` - StringValue string `db:"string_value"` - IntValue int `db:"int_value"` - BoolValue bool `db:"bool_value"` - FloatValue float64 `db:"float_value"` +func TestChangeset(t *testing.T) { - PointerToFloatValue *float64 `db:"ptr_float_value"` - PointerToStringValue *string `db:"ptr_string_value"` + type testStruct struct { + ID int `db:"id,omitempty"` + StringValue string `db:"string_value"` + IntValue int `db:"int_value"` + BoolValue bool `db:"bool_value"` + FloatValue float64 `db:"float_value"` - Entity -} + PointerToFloatValue *float64 `db:"ptr_float_value"` + PointerToStringValue *string `db:"ptr_string_value"` -var ( - stringValue = "hello world!" - floatValue = 5.555 -) + Entity + } -var testCases = []struct { - in Mapper - fn func(interface{}) - out db.Changeset -}{ - { - &testStruct{ - ID: 1, - StringValue: "five", - BoolValue: false, - IntValue: 4, - }, - func(update interface{}) { - u := update.(*testStruct) - u.ID = 2 - u.BoolValue = false - }, - db.Changeset{ - "id": 2, - }, - }, - { - &testStruct{ - ID: 1, - StringValue: "five", - BoolValue: false, - IntValue: 4, - }, - func(update interface{}) { - u := update.(*testStruct) - u.ID = 2 - u.StringValue = "four" - u.FloatValue = 0 - u.BoolValue = false - }, - db.Changeset{ - "id": 2, - "string_value": "four", - }, - }, - { - &testStruct{ - ID: 1, - StringValue: "five", - BoolValue: false, - IntValue: 4, - }, - func(update interface{}) { - u := update.(*testStruct) - u.ID = 2 - u.StringValue = "four" - u.FloatValue = 1.23 - u.BoolValue = false - }, - db.Changeset{ - "id": 2, - "string_value": "four", - "float_value": 1.23, - }, - }, - { - &testStruct{}, - func(update interface{}) { - u := update.(*testStruct) - u.PointerToStringValue = &stringValue - }, - db.Changeset{ - "ptr_string_value": &stringValue, - }, - }, - { - &testStruct{ - PointerToStringValue: &stringValue, - }, - func(update interface{}) { - u := update.(*testStruct) - u.PointerToStringValue = &stringValue - }, - nil, - }, - { - &testStruct{ - FloatValue: 2.123, - PointerToFloatValue: &floatValue, - }, - func(update interface{}) { - u := update.(*testStruct) - u.ID = 9 - }, - db.Changeset{ - "id": 9, - }, - }, - { - &testStruct{}, - nil, - nil, - }, - { - &testStruct{ - FloatValue: 6.6, - }, - nil, - nil, - }, - { - &testStruct{ - FloatValue: 6.6, - }, - func(update interface{}) { - u := update.(*testStruct) - u.FloatValue = 0 - }, - db.Changeset{ - "float_value": float64(0), - }, - }, - { - &testStruct{ - PointerToStringValue: &stringValue, - }, - func(update interface{}) { - u := update.(*testStruct) - u.PointerToStringValue = nil - }, - db.Changeset{ - "ptr_string_value": nil, - }, - }, -} + var ( + stringValue = "hello world!" + floatValue = 5.555 + ) -func TestChangeset(t *testing.T) { + var testCases = []struct { + in Mapper + fn func(interface{}) + out db.Changeset + }{ + { + &testStruct{ + ID: 1, + StringValue: "five", + BoolValue: false, + IntValue: 4, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 2 + u.BoolValue = false + }, + db.Changeset{ + "id": 2, + }, + }, + { + &testStruct{ + ID: 1, + StringValue: "five", + BoolValue: false, + IntValue: 4, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 2 + u.StringValue = "four" + u.FloatValue = 0 + u.BoolValue = false + }, + db.Changeset{ + "id": 2, + "string_value": "four", + }, + }, + { + &testStruct{ + ID: 1, + StringValue: "five", + BoolValue: false, + IntValue: 4, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 2 + u.StringValue = "four" + u.FloatValue = 1.23 + u.BoolValue = false + }, + db.Changeset{ + "id": 2, + "string_value": "four", + "float_value": 1.23, + }, + }, + { + &testStruct{}, + func(update interface{}) { + u := update.(*testStruct) + u.PointerToStringValue = &stringValue + }, + db.Changeset{ + "ptr_string_value": &stringValue, + }, + }, + { + &testStruct{ + PointerToStringValue: &stringValue, + }, + func(update interface{}) { + u := update.(*testStruct) + u.PointerToStringValue = &stringValue + }, + nil, + }, + { + &testStruct{ + FloatValue: 2.123, + PointerToFloatValue: &floatValue, + }, + func(update interface{}) { + u := update.(*testStruct) + u.ID = 9 + }, + db.Changeset{ + "id": 9, + }, + }, + { + &testStruct{}, + nil, + nil, + }, + { + &testStruct{ + FloatValue: 6.6, + }, + nil, + nil, + }, + { + &testStruct{ + FloatValue: 6.6, + }, + func(update interface{}) { + u := update.(*testStruct) + u.FloatValue = 0 + }, + db.Changeset{ + "float_value": float64(0), + }, + }, + { + &testStruct{ + PointerToStringValue: &stringValue, + }, + func(update interface{}) { + u := update.(*testStruct) + u.PointerToStringValue = nil + }, + db.Changeset{ + "ptr_string_value": nil, + }, + }, + } for i := range testCases { - s := testCases[i].in + + { + _, err := s.Changeset() + if err != ErrMapperNotInitialized { + t.Fatal("Expecting error") + } + } + if err := s.Store(s); err != nil { t.Fatal(err) } @@ -169,13 +176,15 @@ func TestChangeset(t *testing.T) { fn(s) } - values, err := s.Changeset() - if err != nil { - t.Fatal(err) - } + { + values, err := s.Changeset() + if err != nil { + t.Fatal(err) + } - if !reflect.DeepEqual(testCases[i].out, values) { - t.Fatal(fmt.Sprintf("test: %v, expected: %#v, got: %#v", i, testCases[i].out, values)) + if !reflect.DeepEqual(testCases[i].out, values) { + t.Fatal(fmt.Sprintf("test: %v, expecting: %#v, got: %#v", i, testCases[i].out, values)) + } } } } diff --git a/lib/sqlbuilder/mapper_test.go b/lib/sqlbuilder/mapper_test.go new file mode 100644 index 00000000..c9155a1f --- /dev/null +++ b/lib/sqlbuilder/mapper_test.go @@ -0,0 +1,206 @@ +package sqlbuilder + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "upper.io/db.v3" +) + +func TestMapper(t *testing.T) { + + type exhibitA struct { + ID int `db:"id,omitempty"` + StringValue string `db:"string_value"` + IntValue int `db:"int_value"` + BoolValue bool `db:"bool_value"` + FloatValue float64 `db:"float_value"` + + PointerToFloatValue *float64 `db:"ptr_float_value"` + PointerToStringValue *string `db:"ptr_string_value"` + } + + type exhibitB struct { + ID int `db:"id,omitempty"` + StringValue string `db:"string_value"` + IntValue int `db:"int_value"` + BoolValue bool `db:"bool_value"` + FloatValue float64 `db:"float_value"` + + Entity + } + + testCases := []struct { + in interface{} + + prepareFn func(interface{}) error + + outFields []string + outValues []interface{} + outErr error + }{ + { + in: &exhibitA{ID: 5}, + + outFields: []string{ + "bool_value", + "float_value", + "id", + "int_value", + "ptr_float_value", + "ptr_string_value", + "string_value", + }, + outValues: []interface{}{ + false, + float64(0), + 5, + 0, + nil, + nil, + "", + }, + }, + { + in: exhibitA{ + BoolValue: true, + FloatValue: 1.2, + StringValue: "hurray", + }, + + outFields: []string{ + "bool_value", + "float_value", + "int_value", + "ptr_float_value", + "ptr_string_value", + "string_value", + }, + outValues: []interface{}{ + true, + float64(1.2), + 0, + nil, + nil, + "hurray", + }, + }, + { + in: map[string]string{"foo": "bar"}, + + outFields: []string{"foo"}, + outValues: []interface{}{"bar"}, + }, + { + in: &map[string]string{"foo": "bar"}, + + outFields: []string{"foo"}, + outValues: []interface{}{"bar"}, + }, + { + in: nil, + + outFields: []string(nil), + outValues: []interface{}(nil), + }, + { + in: db.Changeset{"foo": "bar"}, + + outFields: []string{"foo"}, + outValues: []interface{}{"bar"}, + }, + { + in: &db.Changeset{"foo": "bar"}, + + prepareFn: func(in interface{}) error { + changeset := in.(*db.Changeset) + (*changeset)["foo"] = "baz" + return nil + }, + + outFields: []string{"foo"}, + outValues: []interface{}{"baz"}, + }, + { + in: exhibitB{ + StringValue: "Hello", + }, + + outFields: []string{ + "bool_value", + "float_value", + "int_value", + "string_value", + }, + outValues: []interface{}{ + false, + float64(0), + 0, + "Hello", + }, + }, + { + in: &exhibitB{ + StringValue: "Hello", + }, + + prepareFn: func(in interface{}) error { + data := in.(*exhibitB) + if err := data.Store(data); err != nil { + return err + } + data.BoolValue = true + data.StringValue = "World" + return nil + }, + + outFields: []string{ + "bool_value", + "string_value", + }, + outValues: []interface{}{ + true, + "World", + }, + }, + { + in: &exhibitB{ + StringValue: "Hello", + }, + + prepareFn: func(in interface{}) error { + data := in.(*exhibitB) + if err := data.Store(data); err != nil { + return err + } + data.BoolValue = true + data.StringValue = "World" + if err := data.Store(data); err != nil { + return err + } + return nil + }, + + outFields: []string{}, + outValues: []interface{}{}, + }, + } + + for _, test := range testCases { + if fn := test.prepareFn; fn != nil { + err := fn(test.in) + assert.NoError(t, err) + } + + fields, values, err := Map(test.in, nil) + + assert.Equal(t, test.outFields, fields) + assert.Equal(t, test.outValues, values) + + if test.outErr != nil { + assert.Error(t, test.outErr, err) + } else { + assert.NoError(t, err) + } + } +} diff --git a/postgresql/custom_types_test.go b/postgresql/custom_types_test.go index 37a814bc..a5e09087 100644 --- a/postgresql/custom_types_test.go +++ b/postgresql/custom_types_test.go @@ -2,8 +2,9 @@ package postgresql import ( "encoding/json" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) type testStruct struct { diff --git a/postgresql/local_test.go b/postgresql/local_test.go index d10f2140..b0024819 100644 --- a/postgresql/local_test.go +++ b/postgresql/local_test.go @@ -196,6 +196,16 @@ func TestNonTrivialSubqueries(t *testing.T) { sess := mustOpen() defer sess.Close() + artist := sess.Collection("artist") + + err := artist.Truncate() + assert.NoError(t, err) + + _, err = artist.Insert(map[string]interface{}{ + "name": "Artist", + }) + assert.NoError(t, err) + { q, err := sess.Query(`WITH test AS (?) ?`, sess.Select("id AS foo").From("artist"), From 59c6a32a92cc4c5af89eb325cea1b504542e8884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= Date: Sun, 30 Jul 2017 18:35:31 +0000 Subject: [PATCH 4/5] lib/sqlbuilder: add docstrings for Entity --- lib/sqlbuilder/entity.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/sqlbuilder/entity.go b/lib/sqlbuilder/entity.go index 8beb951f..c52c9ea3 100644 --- a/lib/sqlbuilder/entity.go +++ b/lib/sqlbuilder/entity.go @@ -9,6 +9,9 @@ import ( var ErrMapperNotInitialized = errors.New("Mapper not initialized") +// Mapper defines methods for structs that can keep track of their values at +// some point. Store is used to record the state of a struct and Changeset is +// used to return the differences between that state and the current state. type Mapper interface { Store(interface{}) error Changeset() (db.Changeset, error) @@ -16,6 +19,15 @@ type Mapper interface { changesetWithOptions(options *MapOptions) (db.Changeset, error) } +// Entity can be embedded by structs in order for them to keep track of changes +// automatically. +// +// Example: +// +// type Foo struct { +// ... +// sqlbuilder.Entity +// } type Entity struct { initialValues db.Changeset ref interface{} @@ -50,10 +62,13 @@ func (e *Entity) changesetWithOptions(options *MapOptions) (db.Changeset, error) return changeset, nil } +// Changeset returns the differences between the current state and the last +// recorded state. func (e *Entity) Changeset() (db.Changeset, error) { return e.changesetWithOptions(nil) } +// Store records the current state of the struct. func (e *Entity) Store(v interface{}) error { cols, vals, err := doMap(v, nil) if err != nil { From edb382689919f8a841ea23ee5e65edb028ce047e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= Date: Sun, 30 Jul 2017 18:44:10 +0000 Subject: [PATCH 5/5] db: add description Changeset --- db.go | 1 + 1 file changed, 1 insertion(+) diff --git a/db.go b/db.go index 3ac042dc..e4d0b402 100644 --- a/db.go +++ b/db.go @@ -172,6 +172,7 @@ type Unmarshaler interface { UnmarshalDB(interface{}) error } +// Changeset represents the differences between two states of the same struct. type Changeset map[string]interface{} // Cond is a map that defines conditions for a query and satisfies the