From a63af61b9738ed024b703b100071a990f2e1eaca Mon Sep 17 00:00:00 2001 From: lanceadd <1196661499@qq.com> Date: Wed, 25 Feb 2026 13:37:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(database):=20=E6=B7=BB=E5=8A=A0OmitZero?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=94=A8=E4=BA=8E=E8=BF=87=E6=BB=A4=E9=9B=B6?= =?UTF-8?q?=E5=80=BC=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在internal/empty包中新增IsZero函数,用于检查值是否为零值 - 为数据库查询模型添加OmitZero、OmitZeroWhere和OmitZeroData选项 - 实现对零值参数的自动过滤,不影响非nil空切片/映射的处理 - 新增相关单元测试验证OmitZero功能的正确性 - 扩展formatWhereHolder支持零值过滤逻辑 - 更新数据模型构建器以传递零值过滤选项 --- .../drivers/mysql/mysql_z_unit_model_test.go | 143 ++++++++++++++++++ database/gdb/gdb_func.go | 39 +++++ database/gdb/gdb_model_builder.go | 2 + database/gdb/gdb_model_option.go | 30 ++++ database/gdb/gdb_model_select.go | 1 + database/gdb/gdb_model_utility.go | 12 ++ internal/empty/empty.go | 73 +++++++++ internal/empty/empty_z_unit_test.go | 65 ++++++++ 8 files changed, 365 insertions(+) diff --git a/contrib/drivers/mysql/mysql_z_unit_model_test.go b/contrib/drivers/mysql/mysql_z_unit_model_test.go index cd56d5d1648..70382a80683 100644 --- a/contrib/drivers/mysql/mysql_z_unit_model_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_model_test.go @@ -1707,6 +1707,149 @@ func Test_Model_OmitNil(t *testing.T) { }) } +func Test_Model_OmitZero(t *testing.T) { + table := fmt.Sprintf(`table_%s`, gtime.TimestampNanoStr()) + tableSql := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(45) NOT NULL, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, table) + if _, err := db.Exec(ctx, tableSql); err != nil { + gtest.Error(err) + } + defer dropTable(table) + + // OmitZero filters both zero int and empty string, leaving no data -> error + gtest.C(t, func(t *gtest.T) { + _, err := db.Model(table).OmitZero().Data(g.Map{ + "id": 0, + "name": "", + }).Save() + t.AssertNE(err, nil) + }) + // OmitZeroData: same behavior for data fields + gtest.C(t, func(t *gtest.T) { + _, err := db.Model(table).OmitZeroData().Data(g.Map{ + "id": 0, + "name": "", + }).Save() + t.AssertNE(err, nil) + }) + // OmitZeroData with non-zero values: insert succeeds, 1 row affected + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).OmitZeroData().Data(g.Map{ + "id": 1, + "name": "test", + }).Save() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + }) + // OmitZeroWhere only filters where, not data: insert succeeds, 1 row affected + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).OmitZeroWhere().Data(g.Map{ + "id": 2, + "name": "test2", + }).Save() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + }) +} + +func Test_Model_OmitZeroWhere(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // Basic type where. + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id", 0).Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + }) + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).OmitZeroWhere().Where("id", 0).Count() + t.AssertNil(err) + t.Assert(count, int64(TableSize)) + }) + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).OmitZeroWhere().Where("id", 0).Where("nickname", "").Count() + t.AssertNil(err) + t.Assert(count, int64(TableSize)) + }) + // Slice where: non-nil empty slice is NOT treated as zero by OmitZeroWhere (unlike OmitEmptyWhere). + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id", g.Slice{1, 2, 3}).Count() + t.AssertNil(err) + t.Assert(count, int64(3)) + }) + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id", g.Slice{}).Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + }) + // OmitZeroWhere does NOT filter non-nil empty slice, result is still 0. + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).OmitZeroWhere().Where("id", g.Slice{}).Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + }) + // Struct where: nil slice fields are zero, so they get filtered. + gtest.C(t, func(t *gtest.T) { + type Input struct { + Id []int + Name []string + } + count, err := db.Model(table).Where(Input{}).OmitZeroWhere().Count() + t.AssertNil(err) + t.Assert(count, int64(TableSize)) + }) + // Map where: non-nil empty slice is NOT filtered by OmitZeroWhere. + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where(g.Map{ + "id": []int{}, + }).OmitZeroWhere().Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + }) +} + +func Test_Builder_OmitZeroWhere(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id", 1).Count() + t.AssertNil(err) + t.Assert(count, int64(1)) + }) + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id", 0).OmitZeroWhere().Count() + t.AssertNil(err) + t.Assert(count, int64(TableSize)) + }) + // Builder inherits OmitZeroWhere option + gtest.C(t, func(t *gtest.T) { + builder := db.Model(table).OmitZeroWhere().Builder() + count, err := db.Model(table).Where( + builder.Where("id", 0), + ).Count() + t.AssertNil(err) + t.Assert(count, int64(TableSize)) + }) + // OmitZeroWhere does NOT filter non-nil empty slice (key difference from OmitEmptyWhere) + gtest.C(t, func(t *gtest.T) { + builder := db.Model(table).OmitZeroWhere().Builder() + count, err := db.Model(table).Where( + builder.Where("id", g.Slice{}), + ).Count() + t.AssertNil(err) + t.Assert(count, int64(0)) + }) +} + func Test_Model_FieldsEx(t *testing.T) { table := createInitTable() defer dropTable(table) diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 437c981f8fe..bfd44aac48a 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -427,6 +427,7 @@ func GetPrimaryKeyCondition(primary string, where ...any) (newWhereCondition []a type formatWhereHolderInput struct { WhereHolder OmitNil bool + OmitZero bool OmitEmpty bool Schema string Table string // Table is used for fields mapping and filtering internally. @@ -457,6 +458,25 @@ func isKeyValueCanBeOmitEmpty(omitEmpty bool, whereType string, key, value any) return false } +func isKeyValueCanBeOmitZero(omitZero bool, whereType string, key, value any) bool { + if !omitZero { + return false + } + switch whereType { + case whereHolderTypeNoArgs: + return false + + case whereHolderTypeIn: + return empty.IsZero(value) + + default: + if gstr.Count(gconv.String(key), "?") == 0 && empty.IsZero(value) { + return true + } + } + return false +} + // formatWhereHolder formats where statement and its arguments for `Where` and `Having` statements. func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (newWhere string, newArgs []any) { var ( @@ -472,6 +492,9 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n if in.OmitNil && empty.IsNil(value) { continue } + if in.OmitZero && empty.IsZero(value) { + continue + } if in.OmitEmpty && empty.IsEmpty(value) { continue } @@ -502,6 +525,9 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n if in.OmitNil && empty.IsNil(value) { return true } + if in.OmitZero && empty.IsZero(value) { + return true + } if in.OmitEmpty && empty.IsEmpty(value) { return true } @@ -512,6 +538,7 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n Key: ketStr, Value: value, OmitEmpty: in.OmitEmpty, + OmitZero: in.OmitZero, Prefix: in.Prefix, Type: in.Type, }) @@ -556,6 +583,9 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n if in.OmitNil && empty.IsNil(foundValue) { continue } + if in.OmitZero && empty.IsZero(foundValue) { + continue + } if in.OmitEmpty && empty.IsEmpty(foundValue) { continue } @@ -566,6 +596,7 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n Key: foundKey, Value: foundValue, OmitEmpty: in.OmitEmpty, + OmitZero: in.OmitZero, Prefix: in.Prefix, Type: in.Type, }) @@ -583,6 +614,9 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n if isKeyValueCanBeOmitEmpty(in.OmitEmpty, in.Type, in.Where, omitEmptyCheckValue) { return } + if isKeyValueCanBeOmitZero(in.OmitZero, in.Type, in.Where, omitEmptyCheckValue) { + return + } // Usually a string. whereStr := gstr.Trim(gconv.String(in.Where)) // Is `whereStr` a field name which composed as a key-value condition? @@ -597,6 +631,7 @@ func formatWhereHolder(ctx context.Context, db DB, in formatWhereHolderInput) (n Key: whereStr, Value: in.Args[0], OmitEmpty: in.OmitEmpty, + OmitZero: in.OmitZero, Prefix: in.Prefix, Type: in.Type, }) @@ -709,6 +744,7 @@ type formatWhereKeyValueInput struct { Value any // The field value, can be any types. Type string // The value in Where type. OmitEmpty bool // Ignores current condition key if `value` is empty. + OmitZero bool // Ignores current condition key if `value` is zero value of its type. Prefix string // Field prefix, eg: "user", "order", etc. } @@ -721,6 +757,9 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []any) { if isKeyValueCanBeOmitEmpty(in.OmitEmpty, in.Type, quotedKey, in.Value) { return in.Args } + if isKeyValueCanBeOmitZero(in.OmitZero, in.Type, quotedKey, in.Value) { + return in.Args + } if in.Prefix != "" && !gstr.Contains(quotedKey, ".") { quotedKey = in.Prefix + "." + quotedKey } diff --git a/database/gdb/gdb_model_builder.go b/database/gdb/gdb_model_builder.go index d775a52bfeb..f2fe04d70e0 100644 --- a/database/gdb/gdb_model_builder.go +++ b/database/gdb/gdb_model_builder.go @@ -64,6 +64,7 @@ func (b *WhereBuilder) Build() (conditionWhere string, conditionArgs []any) { newWhere, newArgs := formatWhereHolder(ctx, b.model.db, formatWhereHolderInput{ WhereHolder: holder, OmitNil: b.model.option&optionOmitNilWhere > 0, + OmitZero: b.model.option&optionOmitZeroWhere > 0, OmitEmpty: b.model.option&optionOmitEmptyWhere > 0, Schema: b.model.schema, Table: tableForMappingAndFiltering, @@ -83,6 +84,7 @@ func (b *WhereBuilder) Build() (conditionWhere string, conditionArgs []any) { newWhere, newArgs := formatWhereHolder(ctx, b.model.db, formatWhereHolderInput{ WhereHolder: holder, OmitNil: b.model.option&optionOmitNilWhere > 0, + OmitZero: b.model.option&optionOmitZeroWhere > 0, OmitEmpty: b.model.option&optionOmitEmptyWhere > 0, Schema: b.model.schema, Table: tableForMappingAndFiltering, diff --git a/database/gdb/gdb_model_option.go b/database/gdb/gdb_model_option.go index 1565cedf28f..bb90b77b263 100644 --- a/database/gdb/gdb_model_option.go +++ b/database/gdb/gdb_model_option.go @@ -9,12 +9,15 @@ package gdb const ( optionOmitNil = optionOmitNilWhere | optionOmitNilData optionOmitEmpty = optionOmitEmptyWhere | optionOmitEmptyData + optionOmitZero = optionOmitZeroWhere | optionOmitZeroData optionOmitNilDataInternal = optionOmitNilData | optionOmitNilDataList // this option is used internally only for ForDao feature. optionOmitEmptyWhere = 1 << iota // 8 optionOmitEmptyData // 16 optionOmitNilWhere // 32 optionOmitNilData // 64 optionOmitNilDataList // 128 + optionOmitZeroWhere // 256 + optionOmitZeroData // 512 ) // OmitEmpty sets optionOmitEmpty option for the model, which automatically filers @@ -71,3 +74,30 @@ func (m *Model) OmitNilData() *Model { model.option = model.option | optionOmitNilData return model } + +// OmitZero sets optionOmitZero option for the model, which automatically filters +// the data and where parameters for `zero` values of their types. +// Unlike OmitEmpty, it does NOT treat non-nil empty slice/map as zero. +func (m *Model) OmitZero() *Model { + model := m.getModel() + model.option = model.option | optionOmitZero + return model +} + +// OmitZeroWhere sets optionOmitZeroWhere option for the model, which automatically filters +// the Where/Having parameters for `zero` values of their types. +// Unlike OmitEmptyWhere, it does NOT treat non-nil empty slice/map as zero. +func (m *Model) OmitZeroWhere() *Model { + model := m.getModel() + model.option = model.option | optionOmitZeroWhere + return model +} + +// OmitZeroData sets optionOmitZeroData option for the model, which automatically filters +// the Data parameters for `zero` values of their types. +// Unlike OmitEmptyData, it does NOT treat non-nil empty slice/map as zero. +func (m *Model) OmitZeroData() *Model { + model := m.getModel() + model.option = model.option | optionOmitZeroData + return model +} diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 7d362ac1cbd..df560373127 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -893,6 +893,7 @@ func (m *Model) formatCondition( havingStr, havingArgs := formatWhereHolder(ctx, m.db, formatWhereHolderInput{ WhereHolder: havingHolder, OmitNil: m.option&optionOmitNilWhere > 0, + OmitZero: m.option&optionOmitZeroWhere > 0, OmitEmpty: m.option&optionOmitEmptyWhere > 0, Schema: m.schema, Table: m.tables, diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index 1ced41c39b4..3fe7a63cf5f 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -192,6 +192,18 @@ func (m *Model) doMappingAndFilterForInsertOrUpdateDataMap(data Map, allowOmitEm data = tempMap } + // Remove key-value pairs of which the value is zero value of its type. + if allowOmitEmpty && m.option&optionOmitZeroData > 0 { + tempMap := make(Map, len(data)) + for k, v := range data { + if empty.IsZero(v) { + continue + } + tempMap[k] = v + } + data = tempMap + } + // Remove key-value pairs of which the value is empty. if allowOmitEmpty && m.option&optionOmitEmptyData > 0 { tempMap := make(Map, len(data)) diff --git a/internal/empty/empty.go b/internal/empty/empty.go index 1db709af5bc..32cb4c1bc23 100644 --- a/internal/empty/empty.go +++ b/internal/empty/empty.go @@ -34,6 +34,11 @@ type iTime interface { IsZero() bool } +// iIsZero is used for type assert api for IsZero(). +type iIsZero interface { + IsZero() bool +} + // IsEmpty checks whether given `value` empty. // It returns true if `value` is in: 0, nil, false, "", len(slice/map/chan) == 0, // or else it returns false. @@ -241,3 +246,71 @@ func IsNil(value any, traceSource ...bool) bool { } return false } + +// IsZero checks whether given `value` is zero value of its type. +// It returns true if `value` is in: nil, 0, false, "", nil pointer, nil slice/map/chan, +// zero struct, or implements IsZero() bool interface and returns true. +// Unlike IsEmpty, it does NOT treat non-nil empty slice/map/chan as zero. +// +// The parameter `traceSource` is used for tracing to the source variable if given `value` is type of pointer +// that also points to a pointer. It returns true if the source is zero when `traceSource` is true. +// Note that it might use reflect feature which affects performance a little. +func IsZero(value any, traceSource ...bool) bool { + if value == nil { + return true + } + // It firstly checks the variable as common types using assertion to enhance the performance, + // and then using reflection. + switch result := value.(type) { + case int: + return result == 0 + case int8: + return result == 0 + case int16: + return result == 0 + case int32: + return result == 0 + case int64: + return result == 0 + case uint: + return result == 0 + case uint8: + return result == 0 + case uint16: + return result == 0 + case uint32: + return result == 0 + case uint64: + return result == 0 + case float32: + return result == 0 + case float64: + return result == 0 + case bool: + return !result + case string: + return result == "" + + default: + // Finally, using reflect. + var rv reflect.Value + if v, ok := value.(reflect.Value); ok { + rv = v + } else { + rv = reflect.ValueOf(value) + // Check IsZero() interface for non-reflect.Value inputs. + if f, ok := value.(iIsZero); ok { + return f.IsZero() + } + } + if !rv.IsValid() { + return true + } + if rv.Kind() == reflect.Pointer { + if len(traceSource) > 0 && traceSource[0] { + return IsZero(rv.Elem()) + } + } + return rv.IsZero() + } +} diff --git a/internal/empty/empty_z_unit_test.go b/internal/empty/empty_z_unit_test.go index 6acb52638f9..2f6bcab0bf3 100644 --- a/internal/empty/empty_z_unit_test.go +++ b/internal/empty/empty_z_unit_test.go @@ -163,3 +163,68 @@ func Test_Issue3362(t *testing.T) { t.Assert(empty.IsNil(&i, true), true) }) } + +func TestIsZero(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // true - zero values + t.Assert(empty.IsZero(nil), true) + t.Assert(empty.IsZero(0), true) + t.Assert(empty.IsZero(int8(0)), true) + t.Assert(empty.IsZero(int16(0)), true) + t.Assert(empty.IsZero(int32(0)), true) + t.Assert(empty.IsZero(int64(0)), true) + t.Assert(empty.IsZero(uint(0)), true) + t.Assert(empty.IsZero(uint8(0)), true) + t.Assert(empty.IsZero(uint16(0)), true) + t.Assert(empty.IsZero(uint32(0)), true) + t.Assert(empty.IsZero(uint64(0)), true) + t.Assert(empty.IsZero(float32(0)), true) + t.Assert(empty.IsZero(float64(0)), true) + t.Assert(empty.IsZero(false), true) + t.Assert(empty.IsZero(""), true) + t.Assert(empty.IsZero(time.Time{}), true) + + // true - nil pointer, nil slice, nil map + var nilSlice []int + var nilMap map[string]int + var nilPtr *int + t.Assert(empty.IsZero(nilSlice), true) + t.Assert(empty.IsZero(nilMap), true) + t.Assert(empty.IsZero(nilPtr), true) + + // false - non-zero values + t.Assert(empty.IsZero(1), false) + t.Assert(empty.IsZero(int8(1)), false) + t.Assert(empty.IsZero(int16(1)), false) + t.Assert(empty.IsZero(int32(1)), false) + t.Assert(empty.IsZero(int64(1)), false) + t.Assert(empty.IsZero(uint(1)), false) + t.Assert(empty.IsZero(uint8(1)), false) + t.Assert(empty.IsZero(uint16(1)), false) + t.Assert(empty.IsZero(uint32(1)), false) + t.Assert(empty.IsZero(uint64(1)), false) + t.Assert(empty.IsZero(float32(1)), false) + t.Assert(empty.IsZero(float64(1)), false) + t.Assert(empty.IsZero(true), false) + t.Assert(empty.IsZero("hello"), false) + t.Assert(empty.IsZero(time.Now()), false) + + // KEY DIFFERENCE from IsEmpty: + // Non-nil empty slice/map are NOT zero (but ARE empty). + emptySlice := make([]int, 0) + emptyMap := make(map[string]int) + t.Assert(empty.IsZero(emptySlice), false) + t.Assert(empty.IsZero(emptyMap), false) + // Verify IsEmpty treats them as empty for comparison. + t.Assert(empty.IsEmpty(emptySlice), true) + t.Assert(empty.IsEmpty(emptyMap), true) + }) + + // Test with traceSource for pointer. + gtest.C(t, func(t *gtest.T) { + var i *int + t.Assert(empty.IsZero(i), true) + t.Assert(empty.IsZero(&i), false) + t.Assert(empty.IsZero(&i, true), true) + }) +}