mirror of
https://gitee.com/johng/gf
synced 2026-07-06 13:42:46 +08:00
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:
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
Reference in New Issue
Block a user