From 4598800f87920d17961bcf8c75bf599f2643caa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Tue, 13 Dec 2022 22:58:19 -0300 Subject: [PATCH 1/5] Add nullable modifier --- internal/modifiers/global_modifiers.go | 2 ++ internal/modifiers/json_modifier.go | 7 +++++++ .../{skip_modifiers.go => skip_and_keep_modifiers.go} | 4 ++++ internal/structs/structs.go | 2 +- ksqlmodifiers/attr_modifier.go | 4 ++++ 5 files changed, 18 insertions(+), 1 deletion(-) rename internal/modifiers/{skip_modifiers.go => skip_and_keep_modifiers.go} (75%) diff --git a/internal/modifiers/global_modifiers.go b/internal/modifiers/global_modifiers.go index 0b04d62..9b10bcb 100644 --- a/internal/modifiers/global_modifiers.go +++ b/internal/modifiers/global_modifiers.go @@ -19,6 +19,7 @@ func init() { // This one is useful for serializing/desserializing structs: modifiers.Store("json", jsonModifier) + modifiers.Store("json/nullable", jsonNullableModifier) // This next two are useful for the UpdatedAt and Created fields respectively: // They only work on time.Time attributes and will set the attribute to time.Now(). @@ -29,6 +30,7 @@ func init() { // to test the feature of skipping updates, inserts and queries. modifiers.Store("skipUpdates", skipUpdatesModifier) modifiers.Store("skipInserts", skipInsertsModifier) + modifiers.Store("nullable", nullableModifier) } // RegisterAttrModifier allow users to add custom modifiers on startup diff --git a/internal/modifiers/json_modifier.go b/internal/modifiers/json_modifier.go index ac5cc48..d46af64 100644 --- a/internal/modifiers/json_modifier.go +++ b/internal/modifiers/json_modifier.go @@ -39,3 +39,10 @@ var jsonModifier = ksqlmodifiers.AttrModifier{ return b, err }, } + +var jsonNullableModifier = ksqlmodifiers.AttrModifier{ + Nullable: true, + + Scan: jsonModifier.Scan, + Value: jsonModifier.Value, +} diff --git a/internal/modifiers/skip_modifiers.go b/internal/modifiers/skip_and_keep_modifiers.go similarity index 75% rename from internal/modifiers/skip_modifiers.go rename to internal/modifiers/skip_and_keep_modifiers.go index 27d3080..b492892 100644 --- a/internal/modifiers/skip_modifiers.go +++ b/internal/modifiers/skip_and_keep_modifiers.go @@ -9,3 +9,7 @@ var skipInsertsModifier = ksqlmodifiers.AttrModifier{ var skipUpdatesModifier = ksqlmodifiers.AttrModifier{ SkipOnUpdate: true, } + +var nullableModifier = ksqlmodifiers.AttrModifier{ + Nullable: true, +} diff --git a/internal/structs/structs.go b/internal/structs/structs.go index fbf77e9..90c88c5 100644 --- a/internal/structs/structs.go +++ b/internal/structs/structs.go @@ -150,7 +150,7 @@ func StructToMap(obj interface{}) (map[string]interface{}, error) { field := v.Field(i) ft := field.Type() if ft.Kind() == reflect.Ptr { - if field.IsNil() { + if field.IsNil() && !fieldInfo.Modifier.Nullable { continue } diff --git a/ksqlmodifiers/attr_modifier.go b/ksqlmodifiers/attr_modifier.go index ae18aee..72af7b0 100644 --- a/ksqlmodifiers/attr_modifier.go +++ b/ksqlmodifiers/attr_modifier.go @@ -10,6 +10,10 @@ type AttrModifier struct { SkipOnInsert bool SkipOnUpdate bool + // Nullable will make sure that on Insert and Patch operations + // this field will not be ignored even if it is a NULL pointer. + Nullable bool + // Implement these functions if you want to override the default Scan/Value behavior // for the target attribute. Scan AttrScanner From 88167361c142359ba8602c6c90b1f4b9a5ebc8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Thu, 15 Dec 2022 19:48:31 -0300 Subject: [PATCH 2/5] Add tests to the nullable Modifier --- internal/structs/structs.go | 10 ++- ksql.go | 5 +- test_adapters.go | 148 +++++++++++++++++++++++++++++++++++- 3 files changed, 152 insertions(+), 11 deletions(-) diff --git a/internal/structs/structs.go b/internal/structs/structs.go index 90c88c5..5e74f49 100644 --- a/internal/structs/structs.go +++ b/internal/structs/structs.go @@ -150,11 +150,13 @@ func StructToMap(obj interface{}) (map[string]interface{}, error) { field := v.Field(i) ft := field.Type() if ft.Kind() == reflect.Ptr { - if field.IsNil() && !fieldInfo.Modifier.Nullable { - continue + if !field.IsNil() { + field = field.Elem() + } else { + if !fieldInfo.Modifier.Nullable { + continue + } } - - field = field.Elem() } m[fieldInfo.ColumnName] = field.Interface() diff --git a/ksql.go b/ksql.go index 808991a..3249759 100644 --- a/ksql.go +++ b/ksql.go @@ -13,7 +13,6 @@ import ( "github.com/vingarcia/ksql/internal/modifiers" "github.com/vingarcia/ksql/internal/structs" "github.com/vingarcia/ksql/ksqlmodifiers" - "github.com/vingarcia/ksql/ksqltest" ) var selectQueryCache = initializeQueryCache() @@ -631,7 +630,7 @@ func normalizeIDsAsMap(idNames []string, idOrMap interface{}) (idMap map[string] switch t.Kind() { case reflect.Struct: - idMap, err = ksqltest.StructToMap(idOrMap) + idMap, err = structs.StructToMap(idOrMap) if err != nil { return nil, fmt.Errorf("could not get ID(s) from input record: %w", err) } @@ -724,7 +723,7 @@ func buildInsertQuery( info structs.StructInfo, record interface{}, ) (query string, params []interface{}, scanValues []interface{}, err error) { - recordMap, err := ksqltest.StructToMap(record) + recordMap, err := structs.StructToMap(record) if err != nil { return "", nil, nil, err } diff --git a/test_adapters.go b/test_adapters.go index c801c96..89ecb43 100644 --- a/test_adapters.go +++ b/test_adapters.go @@ -3070,6 +3070,142 @@ func ModifiersTest( tt.AssertEqual(t, taggedUser.Name, "Marta Ribeiro") }) }) + + t.Run("nullable modifier", func(t *testing.T) { + t.Run("should prevent null fields from being ignored during insertions", func(t *testing.T) { + db, closer := newDBAdapter(t) + defer closer.Close() + + c := newTestDB(db, driver) + + var taggedUser struct { + ID uint `ksql:"id"` + NullableField *string `ksql:"nullable_field,nullable"` + } + + var untaggedUser struct { + ID uint `ksql:"id"` + NullableField *string `ksql:"nullable_field"` + } + + err := c.Insert(ctx, usersTable, &taggedUser) + tt.AssertNoErr(t, err) + tt.AssertNotEqual(t, taggedUser.ID, 0) + + err = c.QueryOne(ctx, &taggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), taggedUser.ID) + tt.AssertNoErr(t, err) + + err = c.Insert(ctx, usersTable, &untaggedUser) + tt.AssertNoErr(t, err) + tt.AssertNotEqual(t, taggedUser.ID, 0) + + err = c.QueryOne(ctx, &untaggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), untaggedUser.ID) + tt.AssertNoErr(t, err) + + tt.AssertEqual(t, taggedUser.NullableField == nil, true) + tt.AssertEqual(t, untaggedUser.NullableField, nullable.String("not_null")) + }) + + t.Run("should prevent null fields from being ignored during updates", func(t *testing.T) { + db, closer := newDBAdapter(t) + defer closer.Close() + + c := newTestDB(db, driver) + + type userWithNoTags struct { + ID uint `ksql:"id"` + Name string `ksql:"name"` + NullableField *string `ksql:"nullable_field"` + } + untaggedUser := userWithNoTags{ + Name: "Laurinha Ribeiro", + NullableField: nullable.String("fakeValue"), + } + err := c.Insert(ctx, usersTable, &untaggedUser) + tt.AssertNoErr(t, err) + tt.AssertNotEqual(t, untaggedUser.ID, 0) + + type taggedUser struct { + ID uint `ksql:"id"` + Name string `ksql:"name"` + NullableField *string `ksql:"nullable_field,nullable"` + } + u := taggedUser{ + ID: untaggedUser.ID, + Name: "Laura Ribeiro", + NullableField: nil, + } + err = c.Patch(ctx, usersTable, u) + tt.AssertNoErr(t, err) + + var untaggedUser2 userWithNoTags + err = c.QueryOne(ctx, &untaggedUser2, "FROM users WHERE id = "+c.dialect.Placeholder(0), untaggedUser.ID) + tt.AssertNoErr(t, err) + tt.AssertEqual(t, untaggedUser2.Name, "Laura Ribeiro") + tt.AssertEqual(t, untaggedUser2.NullableField == nil, true) + }) + + t.Run("should not alter the value on queries", func(t *testing.T) { + db, closer := newDBAdapter(t) + defer closer.Close() + + c := newTestDB(db, driver) + + type userWithNoTags struct { + ID uint `ksql:"id"` + Name *string `ksql:"name"` + } + untaggedUser := userWithNoTags{ + Name: nullable.String("Marta Ribeiro"), + } + err := c.Insert(ctx, usersTable, &untaggedUser) + tt.AssertNoErr(t, err) + tt.AssertNotEqual(t, untaggedUser.ID, 0) + + var taggedUser struct { + ID uint `ksql:"id"` + Name *string `ksql:"name,nullable"` + } + err = c.QueryOne(ctx, &taggedUser, "FROM users WHERE id = "+c.dialect.Placeholder(0), untaggedUser.ID) + tt.AssertNoErr(t, err) + tt.AssertEqual(t, taggedUser.ID, untaggedUser.ID) + tt.AssertEqual(t, taggedUser.Name, nullable.String("Marta Ribeiro")) + }) + + t.Run("should cause no effect if used on a non pointer field", func(t *testing.T) { + db, closer := newDBAdapter(t) + defer closer.Close() + + c := newTestDB(db, driver) + + type user struct { + ID uint `ksql:"id"` + Name string `ksql:"name,nullable"` + Age int `ksql:"age,nullable"` + } + u1 := user{ + Name: "Marta Ribeiro", + } + err := c.Insert(ctx, usersTable, &u1) + tt.AssertNoErr(t, err) + tt.AssertNotEqual(t, u1.ID, 0) + + err = c.Patch(ctx, usersTable, &struct { + ID uint `ksql:"id"` + Age int `ksql:"age,nullable"` + }{ + ID: u1.ID, + Age: 42, + }) + + var u2 user + err = c.QueryOne(ctx, &u2, "FROM users WHERE id = "+c.dialect.Placeholder(0), u1.ID) + tt.AssertNoErr(t, err) + tt.AssertEqual(t, u2.ID, u1.ID) + tt.AssertEqual(t, u2.Name, "Marta Ribeiro") + tt.AssertEqual(t, u2.Age, 42) + }) + }) }) } @@ -3317,7 +3453,8 @@ func createTables(driver string, connStr string) error { name TEXT, address BLOB, created_at DATETIME, - updated_at DATETIME + updated_at DATETIME, + nullable_field TEXT DEFAULT "not_null" )`) case "postgres": _, err = db.Exec(`CREATE TABLE users ( @@ -3326,7 +3463,8 @@ func createTables(driver string, connStr string) error { name VARCHAR(50), address jsonb, created_at TIMESTAMP, - updated_at TIMESTAMP + updated_at TIMESTAMP, + nullable_field VARCHAR(50) DEFAULT 'not_null' )`) case "mysql": _, err = db.Exec(`CREATE TABLE users ( @@ -3335,7 +3473,8 @@ func createTables(driver string, connStr string) error { name VARCHAR(50), address JSON, created_at DATETIME, - updated_at DATETIME + updated_at DATETIME, + nullable_field VARCHAR(50) DEFAULT "not_null" )`) case "sqlserver": _, err = db.Exec(`CREATE TABLE users ( @@ -3344,7 +3483,8 @@ func createTables(driver string, connStr string) error { name VARCHAR(50), address NVARCHAR(4000), created_at DATETIME, - updated_at DATETIME + updated_at DATETIME, + nullable_field VARCHAR(50) DEFAULT 'not_null' )`) } if err != nil { From 1a1f198803675e5674e21292dc34413596917bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 17 Dec 2022 14:16:12 -0300 Subject: [PATCH 3/5] Fix linter complaint --- test_adapters.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test_adapters.go b/test_adapters.go index 89ecb43..e2921ae 100644 --- a/test_adapters.go +++ b/test_adapters.go @@ -3197,6 +3197,7 @@ func ModifiersTest( ID: u1.ID, Age: 42, }) + tt.AssertNoErr(t, err) var u2 user err = c.QueryOne(ctx, &u2, "FROM users WHERE id = "+c.dialect.Placeholder(0), u1.ID) From 743d021f7af2c08ee9e8dc641f6c2f2cc14d3e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 17 Dec 2022 14:33:21 -0300 Subject: [PATCH 4/5] Improve test by adding comment --- test_adapters.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_adapters.go b/test_adapters.go index e2921ae..12e510c 100644 --- a/test_adapters.go +++ b/test_adapters.go @@ -3078,6 +3078,14 @@ func ModifiersTest( c := newTestDB(db, driver) + // The default value of the column "nullable_field" + // is the string: "not_null". + // + // So the tagged struct below should insert passing NULL + // and the untagged should insert not passing any value + // for this column, thus, only the second one should create + // a recording using the default value. + var taggedUser struct { ID uint `ksql:"id"` NullableField *string `ksql:"nullable_field,nullable"` From 226b9067465fb5404215e2cb43c85e8dede2d378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 17 Dec 2022 14:36:18 -0300 Subject: [PATCH 5/5] Minor fix to test --- test_adapters.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_adapters.go b/test_adapters.go index 12e510c..1632d0d 100644 --- a/test_adapters.go +++ b/test_adapters.go @@ -3100,12 +3100,12 @@ func ModifiersTest( tt.AssertNoErr(t, err) tt.AssertNotEqual(t, taggedUser.ID, 0) - err = c.QueryOne(ctx, &taggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), taggedUser.ID) - tt.AssertNoErr(t, err) - err = c.Insert(ctx, usersTable, &untaggedUser) tt.AssertNoErr(t, err) - tt.AssertNotEqual(t, taggedUser.ID, 0) + tt.AssertNotEqual(t, untaggedUser.ID, 0) + + err = c.QueryOne(ctx, &taggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), taggedUser.ID) + tt.AssertNoErr(t, err) err = c.QueryOne(ctx, &untaggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), untaggedUser.ID) tt.AssertNoErr(t, err)