Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions contrib/drivers/mysql/mysql_z_unit_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions database/gdb/gdb_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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,
})
Expand Down Expand Up @@ -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
}
Expand All @@ -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,
})
Expand All @@ -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?
Expand All @@ -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,
})
Expand Down Expand Up @@ -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.
}

Expand All @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions database/gdb/gdb_model_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions database/gdb/gdb_model_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 9 to +20

Copilot AI Feb 26, 2026

Copy link

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.
)

// OmitEmpty sets optionOmitEmpty option for the model, which automatically filers
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions database/gdb/gdb_model_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions database/gdb/gdb_model_utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading
Loading