fix(databse/gdb): use COUNT(1) if fields number is greater than 1 even when parameter useFieldForCount is true in AllAndCount/ScanAndCount (#4701)

## Summary
Fix bug where `AllAndCount(true)` with multiple fields generates invalid
SQL `COUNT(field1, field2, ...)` causing syntax error.

## Root Cause
When `useFieldForCount=true`, the COUNT query inherits the fields
configuration from the model:
```go
// Before (buggy code)
if !useFieldForCount {
    countModel.fields = []any{Raw("1")}
}
// When useFieldForCount=true, fields remain as ["id", "nickname"]
// Generates: SELECT COUNT(id, nickname) FROM table 
```

## Fix
Always use `COUNT(1)` regardless of `useFieldForCount` parameter since
COUNT() accepts only one argument:
```go
// After (fixed code)
// Always use COUNT(1) for counting, regardless of useFieldForCount.
// COUNT() accepts only one argument, so we can't use multiple fields.
countModel.fields = []any{Raw("1")}
```

Applied to both `AllAndCount()` and `ScanAndCount()` methods.

## Tests
Added `Test_Issue4698` with 5 test cases:
1. AllAndCount(true) with multiple fields
2. AllAndCount(false) with multiple fields (baseline)
3. ScanAndCount with multiple fields
4. AllAndCount with single field
5. AllAndCount with WHERE condition

All tests verify that COUNT generates valid SQL and returns correct
count.

## Related
Fixes #4698
Ref #4703 (discovered during pagination test development)
This commit is contained in:
Jack Ling
2026-02-27 16:12:58 +08:00
committed by GitHub
parent bbdd442954
commit 612e545ae2
4 changed files with 131 additions and 29 deletions

View File

@ -1983,3 +1983,86 @@ func Test_Issue4697(t *testing.T) {
t.AssertNE(result[0]["nickname"], nil)
})
}
// https://github.com/gogf/gf/issues/4698
func Test_Issue4698(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// Test 1: AllAndCount with multiple fields should generate valid COUNT SQL
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(true)
t.AssertNil(err)
t.Assert(count, TableSize)
t.Assert(len(result), TableSize)
t.AssertNE(result[0]["id"], nil)
t.AssertNE(result[0]["nickname"], nil)
t.Assert(result[0]["passport"], nil)
})
// Test 2: AllAndCount(false) with multiple fields
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(false)
t.AssertNil(err)
t.Assert(count, TableSize)
t.Assert(len(result), TableSize)
})
// Test 3: ScanAndCount with multiple fields
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Nickname string
}
var users []User
var total int
err := db.Model(table).Fields("id, nickname").ScanAndCount(&users, &total, true)
t.AssertNil(err)
t.Assert(total, TableSize)
t.Assert(len(users), TableSize)
t.AssertGT(users[0].Id, 0)
t.AssertNE(users[0].Nickname, "")
})
// Test 4: AllAndCount with single field and useFieldForCount=true
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id").AllAndCount(true)
t.AssertNil(err)
t.Assert(count, TableSize)
t.Assert(len(result), TableSize)
t.Assert(len(result[0]), 1)
})
// Test 5: AllAndCount with Where condition
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id, nickname").Where("id<?", 5).AllAndCount(true)
t.AssertNil(err)
t.Assert(count, 4)
t.Assert(len(result), 4)
})
// Test 6: Distinct + AllAndCount(false) should use COUNT(1), not COUNT(DISTINCT 1)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("nickname").Distinct().AllAndCount(false)
t.AssertNil(err)
// COUNT(1) should return total rows, not distinct count
t.Assert(count, TableSize)
t.AssertGT(len(result), 0)
})
// Test 7: Distinct + AllAndCount(true) with single field should use COUNT(DISTINCT nickname)
gtest.C(t, func(t *gtest.T) {
_, count, err := db.Model(table).Fields("nickname").Distinct().AllAndCount(true)
t.AssertNil(err)
// COUNT(DISTINCT nickname) should return distinct count
t.Assert(count, TableSize)
})
// Test 8: Distinct + multiple fields + AllAndCount(true) should fallback to COUNT(1)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id, nickname").Distinct().AllAndCount(true)
t.AssertNil(err)
t.Assert(count, TableSize)
t.Assert(len(result), TableSize)
})
}

View File

@ -20,8 +20,6 @@ import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
@ -698,17 +696,17 @@ func Test_Model_AllAndCount(t *testing.T) {
t.Assert(all[0]["passport"], "user_1")
t.Assert(count, 1)
})
// AllAndCount with Join return CodeDbOperationError
// AllAndCount with Join and multiple fields
// Regression test for #4698 - should use COUNT(1) not COUNT(field1, field2, ...)
gtest.C(t, func(t *gtest.T) {
all, count, err := db.Model(table).As("u1").
LeftJoin(tableName2, "u2", "u2.id=u1.id").
Fields("u1.passport,u1.id,u2.name,u2.age").
Where("u1.id<2").
AllAndCount(true)
t.AssertNE(err, nil)
t.AssertEQ(gerror.Code(err), gcode.CodeDbOperationError)
t.Assert(count, 0)
t.Assert(all, nil)
t.AssertNil(err)
t.Assert(len(all), 1)
t.Assert(count, 1)
})
}
@ -1390,10 +1388,10 @@ func Test_Model_ScanAndCount(t *testing.T) {
Fields("u1.passport,u1.id,u2.name,u2.age").
Where("u1.id<2").
ScanAndCount(&users, &count, true)
t.AssertNE(err, nil)
t.Assert(gerror.Code(err), gcode.CodeDbOperationError)
t.Assert(count, 0)
t.AssertEQ(users, nil)
// Regression test for #4698 - should use COUNT(1) not COUNT(field1, field2, ...)
t.AssertNil(err)
t.Assert(len(users), 1)
t.Assert(count, 1)
})
}

View File

@ -20,8 +20,6 @@ import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
@ -657,17 +655,22 @@ func Test_Model_AllAndCount(t *testing.T) {
t.Assert(all[0]["passport"], "user_1")
t.Assert(count, 1)
})
// AllAndCount with Join return CodeDbOperationError
// AllAndCount with Join and useFieldForCount=true
// Regression test for #4698 - verifies COUNT(1) is used instead of COUNT(multiple fields)
gtest.C(t, func(t *gtest.T) {
all, count, err := db.Model(table).As("u1").
LeftJoin(tableName2, "u2", "u2.id=u1.id").
Fields("u1.passport,u1.id,u2.name,u2.age").
Where("u1.id<2").
AllAndCount(true)
t.AssertNE(err, nil)
t.AssertEQ(gerror.Code(err), gcode.CodeDbOperationError)
t.Assert(count, 0)
t.Assert(all, nil)
t.AssertNil(err)
t.Assert(len(all), 1)
t.Assert(len(all[0]), 4)
t.Assert(all[0]["id"], 1)
t.Assert(all[0]["age"], 18)
t.Assert(all[0]["name"], "table2_1")
t.Assert(all[0]["passport"], "user_1")
t.Assert(count, 1)
})
}
@ -1334,7 +1337,8 @@ func Test_Model_ScanAndCount(t *testing.T) {
t.Assert(count, 1)
t.AssertEQ(users[0].Name, "table2_1")
})
// ScanAndCount with join return CodeDbOperationError
// ScanAndCount with join and useFieldForCount=true
// Regression test for #4698 - verifies COUNT(1) is used instead of COUNT(multiple fields)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
@ -1349,10 +1353,13 @@ func Test_Model_ScanAndCount(t *testing.T) {
Fields("u1.passport,u1.id,u2.name,u2.age").
Where("u1.id<2").
ScanAndCount(&users, &count, true)
t.AssertNE(err, nil)
t.Assert(gerror.Code(err), gcode.CodeDbOperationError)
t.Assert(count, 0)
t.AssertEQ(users, nil)
t.AssertNil(err)
t.Assert(len(users), 1)
t.Assert(count, 1)
t.Assert(users[0].Id, 1)
t.Assert(users[0].Age, 18)
t.Assert(users[0].Name, "table2_1")
t.Assert(users[0].Passport, "user_1")
})
}

View File

@ -52,9 +52,16 @@ func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount in
// Clone the model for counting
countModel := m.Clone()
// If useFieldForCount is false, set the fields to a constant value of 1 for counting
if !useFieldForCount {
countModel.fields = []any{Raw("1")}
// Decide how to build the COUNT() expression:
// - If caller explicitly wants to use the single field expression for counting,
// honor it (e.g. Fields("DISTINCT col") with useFieldForCount = true).
// - Otherwise, clear fields to let Count() use its default COUNT(1),
// avoiding invalid COUNT(field1, field2, ...) with multiple fields,
// or incorrect COUNT(DISTINCT 1) when Distinct() is set.
if useFieldForCount && len(m.fields) == 1 {
countModel.fields = m.fields
} else {
countModel.fields = nil
}
if len(m.pageCacheOption) > 0 {
countModel = countModel.Cache(m.pageCacheOption[0])
@ -341,9 +348,16 @@ func (m *Model) Scan(pointer any, where ...any) error {
func (m *Model) ScanAndCount(pointer any, totalCount *int, useFieldForCount bool) (err error) {
// support Fields with *, example: .Fields("a.*, b.name"). Count sql is select count(1) from xxx
countModel := m.Clone()
// If useFieldForCount is false, set the fields to a constant value of 1 for counting
if !useFieldForCount {
countModel.fields = []any{Raw("1")}
// Decide how to build the COUNT() expression:
// - If caller explicitly wants to use the single field expression for counting,
// honor it (e.g. Fields("DISTINCT col") with useFieldForCount = true).
// - Otherwise, clear fields to let Count() use its default COUNT(1),
// avoiding invalid COUNT(field1, field2, ...) with multiple fields,
// or incorrect COUNT(DISTINCT 1) when Distinct() is set.
if useFieldForCount && len(m.fields) == 1 {
countModel.fields = m.fields
} else {
countModel.fields = nil
}
if len(m.pageCacheOption) > 0 {
countModel = countModel.Cache(m.pageCacheOption[0])