Skip to content

fix(database/gdb): Added OmitZero function to filter zero value parameters#4713

Open
LanceAdd wants to merge 2 commits intogogf:masterfrom
LanceAdd:fix/omitzero
Open

fix(database/gdb): Added OmitZero function to filter zero value parameters#4713
LanceAdd wants to merge 2 commits intogogf:masterfrom
LanceAdd:fix/omitzero

Conversation

@LanceAdd
Copy link
Copy Markdown
Member

功能概述

本次PR在GoFrame框架的数据库模块中添加了全新的OmitZero功能,用于智能过滤零值参数,提供了比现有OmitEmpty更精确的数据控制能力。最近encoding/json中增加了omitzero标签,这里给gdb补充一下。

核心特性

1. 零值检测机制

  • internal/empty包中新增IsZero函数,提供精确的零值判断
  • 支持所有基础类型(int系列、uint系列、float系列、bool、string)
  • 支持时间类型、指针、切片、映射等复合类型的零值检测
  • 支持实现IsZero() bool接口的自定义类型

2. 数据库模型增强

新增三个核心方法:

  • OmitZero(): 同时过滤WHERE和DATA中的零值参数
  • OmitZeroWhere(): 仅过滤WHERE条件中的零值参数
  • OmitZeroData(): 仅过滤DATA数据中的零值参数

3. 关键差异说明

与现有的OmitEmpty功能相比,OmitZero具有重要区别:

  • OmitZero不会将非nil的空切片/映射视为零值
  • OmitEmpty会过滤非nil空切片/映射,而OmitZero不会
  • 提供了更细粒度的数据控制能力

技术实现细节

核心函数实现

IsZero函数设计

func IsZero(value any, traceSource ...bool) bool {
    // 快速路径:常见类型直接判断
    switch result := value.(type) {
    case int, int8, int16, int32, int64:
        return result == 0
    case uint, uint8, uint16, uint32, uint64:
        return result == 0
    case float32, float64:
        return result == 0
    case bool:
        return !result
    case string:
        return result == ""
    default:
        // 反射路径:处理复杂类型
        // 支持IsZero()接口和指针追踪
    }
}

查询构建器集成

修改了formatWhereHolder函数,增加零值过滤逻辑:

  • 新增OmitZero参数控制
  • 在多个条件分支中添加零值检查
  • 保持与现有OmitNilOmitEmpty的兼容性

模型选项扩展

const (
    optionOmitZeroWhere = 1 << iota + 8 // 256
    optionOmitZeroData                  // 512
    optionOmitZero = optionOmitZeroWhere | optionOmitZeroData
)

使用示例

基础用法

// 过滤零值的INSERT操作
result, err := db.Model("user").OmitZeroData().Data(g.Map{
    "id":   0,     // 被过滤(零值)
    "name": "",    // 被过滤(零值)
    "age":  25,    // 保留(非零值)
}).Save()

// WHERE条件零值过滤
count, err := db.Model("user").OmitZeroWhere().Where("id", 0).Count()
// 结果返回全部记录数,因为id=0被过滤掉了

高级场景

// Builder模式继承零值过滤选项
builder := db.Model("user").OmitZeroWhere().Builder()
count, err := db.Model("user").Where(
    builder.Where("id", 0),  // id=0会被过滤
).Count()

向后兼容性

本PR完全向后兼容:

  • 不改变任何现有API行为
  • 新增功能采用可选模式
  • 现有的OmitEmpty功能保持不变
  • 用户可以逐步迁移到新的OmitZero功能

应用场景

适用场景

  1. API参数处理:自动过滤客户端传入的零值参数
  2. 表单数据处理:智能处理用户未填写的字段
  3. 批量更新操作:避免将零值写入数据库
  4. 条件查询优化:自动忽略无效的查询条件

注意事项

  • 当需要区分"空值"和"未设置"时推荐使用OmitZero
  • 对于需要严格空值过滤的场景仍可使用OmitEmpty
  • 非nil空切片/映射在业务逻辑中有意义时应使用OmitZero

- 在internal/empty包中新增IsZero函数,用于检查值是否为零值
- 为数据库查询模型添加OmitZero、OmitZeroWhere和OmitZeroData选项
- 实现对零值参数的自动过滤,不影响非nil空切片/映射的处理
- 新增相关单元测试验证OmitZero功能的正确性
- 扩展formatWhereHolder支持零值过滤逻辑
- 更新数据模型构建器以传递零值过滤选项
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an OmitZero capability to the gdb model layer, enabling filtering of zero values (distinct from existing OmitEmpty) in both query conditions and write data, backed by a new empty.IsZero helper and new unit tests.

Changes:

  • Added empty.IsZero to detect type zero values (and differentiate from IsEmpty for non-nil empty slices/maps).
  • Introduced Model.OmitZero/OmitZeroWhere/OmitZeroData options and threaded them through where/having formatting.
  • Added unit tests for empty.IsZero and MySQL model behavior for OmitZero*.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/empty/empty.go Adds IsZero implementation used by DB filtering.
internal/empty/empty_z_unit_test.go Adds unit tests for IsZero semantics vs IsEmpty.
database/gdb/gdb_model_utility.go Applies OmitZeroData to insert/update map filtering.
database/gdb/gdb_model_select.go Threads OmitZeroWhere into HAVING formatting.
database/gdb/gdb_model_option.go Defines new option bits and adds OmitZero* model APIs.
database/gdb/gdb_model_builder.go Ensures builder inherits OmitZeroWhere behavior.
database/gdb/gdb_func.go Adds OmitZero support in formatWhereHolder/key-value formatting.
contrib/drivers/mysql/mysql_z_unit_model_test.go Adds integration tests for OmitZero* behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/empty/empty.go
Comment on lines +301 to +308
// Check IsZero() interface for non-reflect.Value inputs.
if f, ok := value.(iIsZero); ok {
return f.IsZero()
}
}
if !rv.IsValid() {
return true
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsZero can panic for typed-nil pointers that implement IsZero() bool (e.g. (*time.Time)(nil)). The iIsZero type assertion happens before any nil-pointer/IsNil check, and calling f.IsZero() will dereference the nil pointer for value-receiver methods. Consider first checking IsNil(rv) (or rv.Kind()==reflect.Pointer && rv.IsNil()) before invoking the interface method, or guarding the interface call with a non-nil underlying value check.

Suggested change
// Check IsZero() interface for non-reflect.Value inputs.
if f, ok := value.(iIsZero); ok {
return f.IsZero()
}
}
if !rv.IsValid() {
return true
}
}
if !rv.IsValid() {
return true
}
// Avoid calling IsZero on typed-nil pointers that implement IsZero() bool.
if rv.Kind() == reflect.Pointer && rv.IsNil() {
return true
}
// Check IsZero() interface for non-nil inputs.
if f, ok := value.(iIsZero); ok {
return f.IsZero()
}

Copilot uses AI. Check for mistakes.
Comment thread internal/empty/empty.go
return true
}
if rv.Kind() == reflect.Pointer {
if len(traceSource) > 0 && traceSource[0] {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsZero uses rv.Elem() when traceSource is true, but it does not guard against rv being a nil pointer. Calling IsZero(nilPtr, true) will panic on rv.Elem(). Add a nil check (rv.IsNil()) before Elem(), and return true for nil pointers in this branch (similar to IsNil/IsEmpty behavior).

Suggested change
if len(traceSource) > 0 && traceSource[0] {
if len(traceSource) > 0 && traceSource[0] {
if rv.IsNil() {
return true
}

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +229
// 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)
})
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new IsZero tests don't cover the panic-prone cases introduced by the iIsZero assertion and traceSource path (e.g. (*time.Time)(nil) and calling IsZero(nilPtr, true)). Adding assertions for these cases would prevent regressions and would have caught the current nil-pointer panic paths.

Copilot uses AI. Check for mistakes.
Comment on lines 9 to +20
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
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding optionOmitZero before the 1 << iota flags shifts the iota index, so the actual values of optionOmitEmptyWhere/Data, optionOmitNilWhere/Data, etc. no longer match the inline comments (and optionOmitZeroWhere/Data won’t be 256/512 as documented in the PR description). To keep bit positions stable and comments accurate, put the derived optionOmitNil/Empty/Zero constants in a separate const block or explicitly anchor the iota offset.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants