diff --git a/contrib/drivers/mysql/mysql_issue_test.go b/contrib/drivers/mysql/mysql_issue_test.go index a57ba1ea1bf..1f2e0d5f4ee 100644 --- a/contrib/drivers/mysql/mysql_issue_test.go +++ b/contrib/drivers/mysql/mysql_issue_test.go @@ -7,6 +7,7 @@ package mysql_test import ( + "context" "fmt" "testing" "time" @@ -886,7 +887,8 @@ func Test_Issue3086(t *testing.T) { func Test_Issue3204(t *testing.T) { table := createInitTable() defer dropTable(table) - db.SetDebug(true) + + // where gtest.C(t, func(t *gtest.T) { type User struct { g.Meta `orm:"do:true"` @@ -905,4 +907,95 @@ func Test_Issue3204(t *testing.T) { t.Assert(len(all), 1) t.Assert(all[0]["id"], 2) }) + // data + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id interface{} `orm:"id,omitempty"` + Passport interface{} `orm:"passport,omitempty"` + Password interface{} `orm:"password,omitempty"` + Nickname interface{} `orm:"nickname,omitempty"` + CreateTime interface{} `orm:"create_time,omitempty"` + } + var ( + err error + sqlArray []string + insertId int64 + data = User{ + Id: 20, + Passport: "passport_20", + Password: "", + } + ) + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + insertId, err = db.Ctx(ctx).Model(table).Data(data).InsertAndGetId() + return err + }) + t.AssertNil(err) + t.Assert(insertId, 20) + t.Assert( + gstr.Contains(sqlArray[len(sqlArray)-1], "(`id`,`passport`) VALUES(20,'passport_20')"), + true, + ) + }) + // update data + gtest.C(t, func(t *gtest.T) { + type User struct { + g.Meta `orm:"do:true"` + Id interface{} `orm:"id,omitempty"` + Passport interface{} `orm:"passport,omitempty"` + Password interface{} `orm:"password,omitempty"` + Nickname interface{} `orm:"nickname,omitempty"` + CreateTime interface{} `orm:"create_time,omitempty"` + } + var ( + err error + sqlArray []string + data = User{ + Passport: "passport_1", + Password: "", + Nickname: "", + } + ) + sqlArray, err = gdb.CatchSQL(ctx, func(ctx context.Context) error { + _, err = db.Ctx(ctx).Model(table).Data(data).WherePri(1).Update() + return err + }) + t.AssertNil(err) + t.Assert( + gstr.Contains(sqlArray[len(sqlArray)-1], "SET `passport`='passport_1' WHERE `id`=1"), + true, + ) + }) +} + +// https://github.com/gogf/gf/issues/3218 +func Test_Issue3218(t *testing.T) { + table := "issue3218_sys_config" + array := gstr.SplitAndTrim(gtest.DataContent(`issue3218.sql`), ";") + for _, v := range array { + if _, err := db.Exec(ctx, v); err != nil { + gtest.Error(err) + } + } + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + type SysConfigInfo struct { + Name string `json:"name"` + Value map[string]string `json:"value"` + } + var configData *SysConfigInfo + err := db.Model(table).Scan(&configData) + t.AssertNil(err) + t.Assert(configData, &SysConfigInfo{ + Name: "site", + Value: map[string]string{ + "fixed_page": "", + "site_name": "22", + "version": "22", + "banned_ip": "22", + "filings": "2222", + }, + }) + }) } diff --git a/contrib/drivers/mysql/testdata/issue3218.sql b/contrib/drivers/mysql/testdata/issue3218.sql new file mode 100644 index 00000000000..93b5f9dacc9 --- /dev/null +++ b/contrib/drivers/mysql/testdata/issue3218.sql @@ -0,0 +1,14 @@ +CREATE TABLE `issue3218_sys_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置名称', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '配置值', + `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `name`(`name`(191)) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci; + +-- ---------------------------- +-- Records of sys_config +-- ---------------------------- +INSERT INTO `issue3218_sys_config` VALUES (49, 'site', '{\"banned_ip\":\"22\",\"filings\":\"2222\",\"fixed_page\":\"\",\"site_name\":\"22\",\"version\":\"22\"}', '2023-12-19 14:08:25', '2023-12-19 14:08:25'); \ No newline at end of file diff --git a/database/gdb/gdb_core_structure.go b/database/gdb/gdb_core_structure.go index bcfa5abb19c..161825094d8 100644 --- a/database/gdb/gdb_core_structure.go +++ b/database/gdb/gdb_core_structure.go @@ -60,7 +60,7 @@ func (c *Core) GetFieldType(ctx context.Context, fieldName, table, schema string func (c *Core) ConvertDataForRecord(ctx context.Context, value interface{}, table string) (map[string]interface{}, error) { var ( err error - data = MapOrStructToMapDeep(value, false) + data = MapOrStructToMapDeep(value, true) ) for fieldName, fieldValue := range data { data[fieldName], err = c.db.ConvertValueForField( diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 3fbf1e7de5f..4d2f9b41f91 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -96,7 +96,9 @@ func DBFromCtx(ctx context.Context) DB { return nil } -// ToSQL formats and returns the last one of sql statements in given closure function without truly executing it. +// ToSQL formats and returns the last one of sql statements in given closure function +// WITHOUT TRULY EXECUTING IT. +// Be caution that, all the following sql statements should use the context object passing by function `f`. func ToSQL(ctx context.Context, f func(ctx context.Context) error) (sql string, err error) { var manager = &CatchSQLManager{ SQLArray: garray.NewStrArray(), @@ -108,7 +110,8 @@ func ToSQL(ctx context.Context, f func(ctx context.Context) error) (sql string, return } -// CatchSQL catches and returns all sql statements that are executed in given closure function. +// CatchSQL catches and returns all sql statements that are EXECUTED in given closure function. +// Be caution that, all the following sql statements should use the context object passing by function `f`. func CatchSQL(ctx context.Context, f func(ctx context.Context) error) (sqlArray []string, err error) { var manager = &CatchSQLManager{ SQLArray: garray.NewStrArray(), @@ -210,12 +213,15 @@ func GetInsertOperationByOption(option InsertOption) string { } func anyValueToMapBeforeToRecord(value interface{}) map[string]interface{} { - return gconv.Map(value, gconv.MapOption{Tags: structTagPriority}) + return gconv.Map(value, gconv.MapOption{ + Tags: structTagPriority, + OmitEmpty: true, // To be compatible with old version from v2.6.0. + }) } // DaToMapDeep is deprecated, use MapOrStructToMapDeep instead. func DaToMapDeep(value interface{}) map[string]interface{} { - return MapOrStructToMapDeep(value, false) + return MapOrStructToMapDeep(value, true) } // MapOrStructToMapDeep converts `value` to map type recursively(if attribute struct is embedded). diff --git a/util/gconv/gconv_interface.go b/util/gconv/gconv_interface.go index 9440e978f9a..8ba7361bc2e 100644 --- a/util/gconv/gconv_interface.go +++ b/util/gconv/gconv_interface.go @@ -8,6 +8,11 @@ package gconv import "github.com/gogf/gf/v2/os/gtime" +// iVal is used for type assert api for String(). +type iVal interface { + Val() interface{} +} + // iString is used for type assert api for String(). type iString interface { String() string diff --git a/util/gconv/gconv_map.go b/util/gconv/gconv_map.go index 7627d684077..7d52fcda3e2 100644 --- a/util/gconv/gconv_map.go +++ b/util/gconv/gconv_map.go @@ -29,7 +29,7 @@ type MapOption struct { // a map[string]interface{} type variable. Deep bool - // OmitEmpty ignores the attributes that has json omitempty tag. + // OmitEmpty ignores the attributes that has json `omitempty` tag. OmitEmpty bool // Tags specifies the converted map key name by struct tag name. @@ -64,8 +64,15 @@ func doMapConvert(value interface{}, recursive recursiveType, mustMapReturn bool if value == nil { return nil } - var usedOption = getUsedMapOption(option...) - newTags := StructTagPriority + // It redirects to its underlying value if it has implemented interface iVal. + if v, ok := value.(iVal); ok { + value = v.Val() + } + + var ( + usedOption = getUsedMapOption(option...) + newTags = StructTagPriority + ) switch len(usedOption.Tags) { case 0: // No need handling.