test(contrib/drivers/mariadb): add pagination, error, concurrent, rawtype and sharding tests (#4723)

## Summary

- Port 11 pagination tests: Page/Limit/Offset, combined with
Where/Order, boundary conditions (page 0, large offset), Count with
pagination
- Port 8 error-handling tests: invalid table/field names, syntax errors,
duplicate key, connection errors, error wrapping and message
verification
- Port 5 concurrency tests: parallel read/write with goroutines and
WaitGroup, concurrent transactions, race condition verification
- Port 6 raw-type tests: custom type scanning, time.Time handling,
json.RawMessage, sql.NullString/NullInt64, []byte fields
- Port 6 sharding/table-name tests: dynamic table name via Sharding
callback, table name with prefix, schema.table format

All tests are structurally identical to the MySQL driver baseline. SQL
syntax is standard and shared. Package and import references are adapted
for MariaDB.

ref #4689
This commit is contained in:
Jack Ling
2026-03-12 11:14:48 +08:00
committed by GitHub
parent 6204c132c7
commit 0588009c40
6 changed files with 2761 additions and 3 deletions

View File

@ -30,9 +30,10 @@ const (
)
var (
db gdb.DB
db2 gdb.DB
ctx = context.TODO()
db gdb.DB
db2 gdb.DB
dbInvalid gdb.DB
ctx = context.TODO()
)
func init() {
@ -61,6 +62,19 @@ func init() {
}
db = db.Schema(TestSchema1)
db2 = db.Schema(TestSchema2)
// Invalid db (wrong port for testing error handling).
nodeInvalid := gdb.ConfigNode{
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3317)/?loc=Local&parseTime=true", TestDbPass),
TranTimeout: time.Second * 3,
}
gdb.AddConfigNode("nodeinvalid", nodeInvalid)
if r, err := gdb.NewByGroup("nodeinvalid"); err != nil {
gtest.Error(err)
} else {
dbInvalid = r
}
dbInvalid = dbInvalid.Schema(TestSchema1)
}
func createTable(table ...string) string {

View File

@ -0,0 +1,338 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb_test
import (
"fmt"
"sync"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
)
// Test_Concurrent_Insert tests concurrent Insert operations
func Test_Concurrent_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 10
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
_, err := db.Model(table).Insert(g.Map{
"passport": fmt.Sprintf("user_%d", id),
"password": fmt.Sprintf("pass_%d", id),
"nickname": fmt.Sprintf("name_%d", id),
})
t.AssertNil(err)
}(i + 1)
}
wg.Wait()
// Verify all records inserted
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, concurrency)
})
}
// Test_Concurrent_Update tests concurrent Update operations
func Test_Concurrent_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 5
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
_, err := db.Model(table).Data(g.Map{
"nickname": fmt.Sprintf("updated_%d", id),
}).Where("id", id+1).Update()
t.AssertNil(err)
}(i)
}
wg.Wait()
// Verify updates
for i := 0; i < concurrency; i++ {
one, err := db.Model(table).Where("id", i+1).One()
t.AssertNil(err)
t.Assert(one["nickname"].String(), fmt.Sprintf("updated_%d", i))
}
})
}
// Test_Concurrent_Delete tests concurrent Delete operations
func Test_Concurrent_Delete(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 5
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
_, err := db.Model(table).Where("id", id+1).Delete()
t.AssertNil(err)
}(i)
}
wg.Wait()
// Verify deletions
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize-concurrency)
})
}
// Test_Concurrent_Query tests concurrent Query operations
func Test_Concurrent_Query(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 20
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
result, err := db.Model(table).Where("id", (id%TableSize)+1).One()
t.AssertNil(err)
t.AssertNE(result, nil)
}(i)
}
wg.Wait()
})
}
// Test_Concurrent_Transaction tests concurrent transaction operations
func Test_Concurrent_Transaction(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 10
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
err := db.Transaction(ctx, func(ctx g.Ctx, tx gdb.TX) error {
_, err := tx.Model(table).Insert(g.Map{
"passport": fmt.Sprintf("user_%d", id),
"password": fmt.Sprintf("pass_%d", id),
"nickname": fmt.Sprintf("name_%d", id),
})
return err
})
t.AssertNil(err)
}(i + 1)
}
wg.Wait()
// Verify all transactions committed
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, concurrency)
})
}
// Test_Concurrent_Mixed_Operations tests mixed concurrent operations
func Test_Concurrent_Mixed_Operations(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
operations := 30
wg.Add(operations)
for i := 0; i < operations; i++ {
op := i % 3
switch op {
case 0: // Insert
go func(id int) {
defer wg.Done()
_, _ = db.Model(table).Insert(g.Map{
"passport": fmt.Sprintf("new_user_%d", id),
"password": fmt.Sprintf("new_pass_%d", id),
"nickname": fmt.Sprintf("new_name_%d", id),
})
}(i)
case 1: // Update
go func(id int) {
defer wg.Done()
targetId := (id % TableSize) + 1
_, _ = db.Model(table).Data(g.Map{
"nickname": fmt.Sprintf("concurrent_%d", id),
}).Where("id", targetId).Update()
}(i)
case 2: // Query
go func(id int) {
defer wg.Done()
targetId := (id % TableSize) + 1
_, _ = db.Model(table).Where("id", targetId).One()
}(i)
}
}
wg.Wait()
// Verify database is still consistent
count, err := db.Model(table).Count()
t.AssertNil(err)
t.AssertGT(count, TableSize)
})
}
// Test_Concurrent_Connection_Pool tests connection pool under load
func Test_Concurrent_Connection_Pool(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 50
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
// Each goroutine performs multiple operations
for j := 0; j < 5; j++ {
_, err := db.Model(table).Where("id", (id%TableSize)+1).One()
t.AssertNil(err)
}
}(i)
}
wg.Wait()
})
}
// Test_Concurrent_Schema_Switch tests concurrent schema switching
func Test_Concurrent_Schema_Switch(t *testing.T) {
table1 := createTableWithDb(db, "test_schema_1")
table2 := createTableWithDb(db2, "test_schema_2")
defer dropTableWithDb(db, table1)
defer dropTableWithDb(db2, table2)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 10
wg.Add(concurrency * 2)
for i := 0; i < concurrency; i++ {
// Insert to schema1
go func(id int) {
defer wg.Done()
_, err := db.Model(table1).Insert(g.Map{
"passport": fmt.Sprintf("user_s1_%d", id),
"password": fmt.Sprintf("pass_%d", id),
"nickname": fmt.Sprintf("name_%d", id),
})
t.AssertNil(err)
}(i)
// Insert to schema2
go func(id int) {
defer wg.Done()
_, err := db2.Model(table2).Insert(g.Map{
"passport": fmt.Sprintf("user_s2_%d", id),
"password": fmt.Sprintf("pass_%d", id),
"nickname": fmt.Sprintf("name_%d", id),
})
t.AssertNil(err)
}(i)
}
wg.Wait()
// Verify both schemas
count1, err := db.Model(table1).Count()
t.AssertNil(err)
t.Assert(count1, concurrency)
count2, err := db2.Model(table2).Count()
t.AssertNil(err)
t.Assert(count2, concurrency)
})
}
// Test_Concurrent_Model_Clone tests concurrent model cloning
func Test_Concurrent_Model_Clone(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
baseModel := db.Model(table).Where("id>", 0)
var wg sync.WaitGroup
concurrency := 20
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
// Clone model for each goroutine
m := baseModel.Clone()
result, err := m.Where("id<=", TableSize/2).All()
t.AssertNil(err)
t.AssertGT(len(result), 0)
}(i)
}
wg.Wait()
})
}
// Test_Concurrent_Batch_Insert tests concurrent batch insert operations
func Test_Concurrent_Batch_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var wg sync.WaitGroup
concurrency := 5
batchSize := 10
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(batchId int) {
defer wg.Done()
batch := make([]g.Map, 0, batchSize)
for j := 0; j < batchSize; j++ {
id := batchId*batchSize + j
batch = append(batch, g.Map{
"passport": fmt.Sprintf("batch_user_%d", id),
"password": fmt.Sprintf("pass_%d", id),
"nickname": fmt.Sprintf("name_%d", id),
})
}
_, err := db.Model(table).Data(batch).Insert()
t.AssertNil(err)
}(i)
}
wg.Wait()
// Verify all batch inserts
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, concurrency*batchSize)
})
}

View File

@ -0,0 +1,489 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb_test
import (
"context"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
)
// Test_Model_Insert_NilData tests Insert with nil data
func Test_Model_Insert_NilData(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Data(nil).Insert()
t.AssertNE(err, nil)
})
}
// Test_Model_Insert_EmptyMap tests Insert with empty map
func Test_Model_Insert_EmptyMap(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Data(g.Map{}).Insert()
t.AssertNE(err, nil)
})
}
// Test_Model_Insert_EmptySlice tests Insert with empty slice
func Test_Model_Insert_EmptySlice(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Data(g.Slice{}).Insert()
t.AssertNE(err, nil)
})
}
// Test_Model_Update_NilData tests Update with nil data
func Test_Model_Update_NilData(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Data(nil).Where("id", 1).Update()
t.AssertNE(err, nil)
})
}
// Test_Model_Update_EmptyData tests Update with empty data
func Test_Model_Update_EmptyData(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Data(g.Map{}).Where("id", 1).Update()
t.AssertNE(err, nil)
})
}
// Test_Model_Update_NoWhere tests Update without WHERE clause is rejected by framework
func Test_Model_Update_NoWhere(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Framework safety check: Update without WHERE should return error
_, err := db.Model(table).Data(g.Map{"nickname": "updated"}).Update()
t.AssertNE(err, nil)
})
}
// Test_Model_Delete_NoWhere tests Delete without WHERE clause is rejected by framework
func Test_Model_Delete_NoWhere(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Framework safety check: Delete without WHERE should return error
_, err := db.Model(table).Delete()
t.AssertNE(err, nil)
})
}
// Test_Model_Scan_NilPointer tests Scan with nil pointer
func Test_Model_Scan_NilPointer(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
err := db.Model(table).Where("id", 1).Scan(nil)
t.AssertNE(err, nil)
})
}
// Test_Model_Scan_InvalidPointer tests Scan with invalid pointer type
func Test_Model_Scan_InvalidPointer(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var str string
err := db.Model(table).Where("id", 1).Scan(&str)
t.AssertNE(err, nil)
})
}
// Test_Model_Scan_EmptyResult tests Scan with empty result
func Test_Model_Scan_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
}
// Scan initialized struct with empty result returns sql.ErrNoRows
gtest.C(t, func(t *gtest.T) {
var user User
err := db.Model(table).Where("id > ?", 1000).Scan(&user)
t.AssertNE(err, nil)
})
// Scan nil pointer with empty result returns nil error
gtest.C(t, func(t *gtest.T) {
var user *User
err := db.Model(table).Where("id > ?", 1000).Scan(&user)
t.AssertNil(err)
t.Assert(user, nil)
})
}
// Test_Model_Where_InvalidOperator tests Where with invalid operator
func Test_Model_Where_InvalidOperator(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Invalid SQL should cause error at query time
_, err := db.Model(table).Where("id INVALID_OP ?", 1).All()
t.AssertNE(err, nil)
})
}
// Test_Model_Where_EmptyString tests Where with empty string
func Test_Model_Where_EmptyString(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Where("").All()
t.AssertNil(err)
t.Assert(len(result), TableSize) // Empty WHERE returns all
})
}
// Test_Model_Fields_InvalidField tests Fields with non-existent field
func Test_Model_Fields_InvalidField(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Fields("non_existent_field").All()
t.AssertNE(err, nil)
})
}
// Test_Model_Fields_Empty tests Fields with empty string
// Regression test for #4697: Fields("") should handle empty string gracefully
// https://github.com/gogf/gf/issues/4697
func Test_Model_Fields_Empty(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Fields("").Limit(1).All()
t.AssertNil(err)
t.AssertLE(len(result), 1)
})
}
// Test_Model_Order_InvalidSyntax tests Order with invalid syntax
func Test_Model_Order_InvalidSyntax(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Invalid ORDER BY syntax
_, err := db.Model(table).Order("id INVALID").All()
t.AssertNE(err, nil)
})
}
// Test_Model_Group_UnknownColumn tests Group with non-existent column
func Test_Model_Group_UnknownColumn(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Group("non_existent_field").All()
t.AssertNE(err, nil)
})
}
// Test_Model_TableNotExist tests querying non-existent table
func Test_Model_TableNotExist(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
_, err := db.Model("non_existent_table_xyz").All()
t.AssertNE(err, nil)
})
}
// Test_Model_InvalidTableName tests invalid table name
func Test_Model_InvalidTableName(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Empty table name
_, err := db.Model("").All()
t.AssertNE(err, nil)
})
}
// Test_Model_SQLInjection_Where tests SQL injection prevention in Where
func Test_Model_SQLInjection_Where(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Attempt SQL injection through string column parameter.
// Using string column `nickname` instead of int column `id`,
// because MySQL coerces "1 OR 1=1" to 1 for int columns.
maliciousInput := "1 OR 1=1"
result, err := db.Model(table).Where("nickname = ?", maliciousInput).All()
t.AssertNil(err)
t.Assert(len(result), 0) // Should not return all records
})
gtest.C(t, func(t *gtest.T) {
// Attempt SQL injection with quotes, using string column to avoid
// MySQL implicit int conversion (which would coerce "1'..." to 1)
maliciousInput := "1'; DROP TABLE " + table + "; --"
result, err := db.Model(table).Where("nickname = ?", maliciousInput).All()
t.AssertNil(err)
t.Assert(len(result), 0)
// Table should still exist
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
// Test_Model_SQLInjection_Insert tests SQL injection prevention in Insert
func Test_Model_SQLInjection_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
maliciousData := g.Map{
"id": 1,
"passport": "'; DROP TABLE " + table + "; --",
"password": "pwd",
"nickname": "test",
}
_, err := db.Model(table).Data(maliciousData).Insert()
t.AssertNil(err)
// Verify data was inserted correctly and table still exists
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.AssertNE(one, nil)
t.Assert(one["passport"].String(), "'; DROP TABLE "+table+"; --")
})
}
// Test_Model_SQLInjection_Update tests SQL injection prevention in Update
func Test_Model_SQLInjection_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Use shorter malicious string to fit in nickname column
maliciousData := g.Map{
"nickname": "'; DELETE FROM users; --",
}
_, err := db.Model(table).Data(maliciousData).Where("id", 1).Update()
t.AssertNil(err)
// Verify only one record was updated (parameterized query prevents injection)
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["nickname"].String(), "'; DELETE FROM users; --")
// Other records should still exist (injection was prevented)
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
// Test_Model_Context_Cancelled tests query with cancelled context
func Test_Model_Context_Cancelled(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := db.Model(table).Ctx(ctx).All()
t.AssertNE(err, nil)
t.Assert(gerror.Is(err, context.Canceled), true)
})
}
// Test_Model_Value_EmptyResult tests Value with empty result
func Test_Model_Value_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
value, err := db.Model(table).Where("id > ?", 1000).Value()
t.AssertNil(err)
t.Assert(value.IsEmpty(), true)
})
}
// Test_Model_Array_EmptyResult tests Array with empty result
func Test_Model_Array_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
array, err := db.Model(table).Where("id > ?", 1000).Array()
t.AssertNil(err)
t.Assert(len(array), 0)
})
}
// Test_Model_Count_InvalidTable tests Count on invalid table
func Test_Model_Count_InvalidTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
_, err := db.Model("non_existent_table").Count()
t.AssertNE(err, nil)
})
}
// Test_Model_Max_EmptyResult tests Max with empty result
func Test_Model_Max_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
value, err := db.Model(table).Where("id > ?", 1000).Max("id")
t.AssertNil(err)
t.Assert(value, 0) // Returns 0 for empty result
})
}
// Test_Model_Min_EmptyResult tests Min with empty result
func Test_Model_Min_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
value, err := db.Model(table).Where("id > ?", 1000).Min("id")
t.AssertNil(err)
t.Assert(value, 0) // Returns 0 for empty result
})
}
// Test_Model_Avg_EmptyResult tests Avg with empty result
func Test_Model_Avg_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
value, err := db.Model(table).Where("id > ?", 1000).Avg("id")
t.AssertNil(err)
t.Assert(value, 0) // Returns 0 for empty result
})
}
// Test_Model_Sum_EmptyResult tests Sum with empty result
func Test_Model_Sum_EmptyResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
value, err := db.Model(table).Where("id > ?", 1000).Sum("id")
t.AssertNil(err)
t.Assert(value, 0) // Returns 0 for empty result
})
}
// Test_Model_One_NilResult tests One returning nil
func Test_Model_One_NilResult(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
one, err := db.Model(table).Where("id > ?", 1000).One()
t.AssertNil(err)
t.Assert(one, nil)
})
}
// Test_TX_Rollback_AfterError tests transaction rollback after error
func Test_TX_Rollback_AfterError(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// Insert valid record
_, err := tx.Model(table).Data(g.Map{
"id": 1,
"passport": "pass1",
"password": "pwd1",
"nickname": "name1",
}).Insert()
if err != nil {
return err
}
// Insert duplicate id (should fail)
_, err = tx.Model(table).Data(g.Map{
"id": 1, // Duplicate
"passport": "pass2",
"password": "pwd2",
"nickname": "name2",
}).Insert()
return err // Return error to trigger rollback
})
t.AssertNE(err, nil)
// Verify rollback - table should be empty
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 0)
})
}
// Test_Model_Insert_DuplicateKey tests handling of duplicate key error
func Test_Model_Insert_DuplicateKey(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pass",
"password": "pwd",
"nickname": "name",
}
// First insert should succeed
_, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
// Second insert with same id should fail
_, err = db.Model(table).Data(data).Insert()
t.AssertNE(err, nil)
})
}
// Test_Model_All_InvalidConnection tests query with invalid connection
func Test_Model_All_InvalidConnection(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
if dbInvalid == nil {
t.Skip("dbInvalid not configured")
}
_, err := dbInvalid.Model("test_table").All()
t.AssertNE(err, nil)
})
}

View File

@ -0,0 +1,467 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb_test
import (
"context"
"database/sql"
"fmt"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
const (
TestDbNameSh0 = "test_0"
TestDbNameSh1 = "test_1"
TestTableName = "user"
)
type ShardingUser struct {
Id int
Name string
}
// createShardingDatabase creates test databases and tables for sharding
func createShardingDatabase(t *gtest.T) {
// Create databases
dbs := []string{TestDbNameSh0, TestDbNameSh1}
for _, dbName := range dbs {
sql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", dbName)
_, err := db.Exec(ctx, sql)
t.AssertNil(err)
// Switch to the database
sql = fmt.Sprintf("USE `%s`", dbName)
_, err = db.Exec(ctx, sql)
t.AssertNil(err)
// Create tables
tables := []string{"user_0", "user_1", "user_2", "user_3"}
for _, table := range tables {
sql := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
id int(11) NOT NULL,
name varchar(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`, table)
_, err := db.Exec(ctx, sql)
t.AssertNil(err)
}
}
}
// dropShardingDatabase drops test databases
func dropShardingDatabase(t *gtest.T) {
dbs := []string{TestDbNameSh0, TestDbNameSh1}
for _, dbName := range dbs {
sql := fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", dbName)
_, err := db.Exec(ctx, sql)
t.AssertNil(err)
}
}
func Test_Sharding_Basic(t *testing.T) {
return
gtest.C(t, func(t *gtest.T) {
var (
tablePrefix = "user_"
schemaPrefix = "test_"
)
// Create test databases and tables
createShardingDatabase(t)
defer dropShardingDatabase(t)
// Create sharding configuration
shardingConfig := gdb.ShardingConfig{
Table: gdb.ShardingTableConfig{
Enable: true,
Prefix: tablePrefix,
Rule: &gdb.DefaultShardingRule{
TableCount: 4,
},
},
Schema: gdb.ShardingSchemaConfig{
Enable: true,
Prefix: schemaPrefix,
Rule: &gdb.DefaultShardingRule{
SchemaCount: 2,
},
},
}
// Prepare test data
user := ShardingUser{
Id: 1,
Name: "John",
}
model := db.Model(TestTableName).
Sharding(shardingConfig).
ShardingValue(user.Id).
Safe()
// Test Insert
_, err := model.Data(user).Insert()
t.AssertNil(err)
// Test Select
var result ShardingUser
err = model.Where("id", user.Id).Scan(&result)
t.AssertNil(err)
t.Assert(result.Id, user.Id)
t.Assert(result.Name, user.Name)
// Test Update
_, err = model.Data(g.Map{"name": "John Doe"}).
Where("id", user.Id).
Update()
t.AssertNil(err)
// Verify Update
err = model.Where("id", user.Id).Scan(&result)
t.AssertNil(err)
t.Assert(result.Name, "John Doe")
// Test Delete
_, err = model.Where("id", user.Id).Delete()
t.AssertNil(err)
// Verify Delete
count, err := model.Where("id", user.Id).Count()
t.AssertNil(err)
t.Assert(count, 0)
})
}
// Test_Sharding_Error tests error cases
func Test_Sharding_Error(t *testing.T) {
return
gtest.C(t, func(t *gtest.T) {
// Create test databases and tables
createShardingDatabase(t)
defer dropShardingDatabase(t)
// Test missing sharding value
model := db.Model(TestTableName).
Sharding(gdb.ShardingConfig{
Table: gdb.ShardingTableConfig{
Enable: true,
Prefix: "user_",
Rule: &gdb.DefaultShardingRule{TableCount: 4},
},
}).Safe()
_, err := model.Insert(g.Map{"id": 1, "name": "test"})
t.AssertNE(err, nil)
t.Assert(err.Error(), "sharding value is required when sharding feature enabled")
// Test missing sharding rule
model = db.Model(TestTableName).
Sharding(gdb.ShardingConfig{
Table: gdb.ShardingTableConfig{
Enable: true,
Prefix: "user_",
},
}).
ShardingValue(1)
_, err = model.Insert(g.Map{"id": 1, "name": "test"})
t.AssertNE(err, nil)
t.Assert(err.Error(), "sharding rule is required when sharding feature enabled")
})
}
// Test_Sharding_Complex tests complex sharding scenarios
func Test_Sharding_Complex(t *testing.T) {
return
gtest.C(t, func(t *gtest.T) {
// Create test databases and tables
createShardingDatabase(t)
defer dropShardingDatabase(t)
shardingConfig := gdb.ShardingConfig{
Table: gdb.ShardingTableConfig{
Enable: true,
Prefix: "user_",
Rule: &gdb.DefaultShardingRule{TableCount: 4},
},
Schema: gdb.ShardingSchemaConfig{
Enable: true,
Prefix: "test_",
Rule: &gdb.DefaultShardingRule{SchemaCount: 2},
},
}
users := []ShardingUser{
{Id: 1, Name: "User1"},
{Id: 2, Name: "User2"},
{Id: 3, Name: "User3"},
}
for _, user := range users {
model := db.Model(TestTableName).
Sharding(shardingConfig).
ShardingValue(user.Id).
Safe()
_, err := model.Data(user).Insert()
t.AssertNil(err)
}
// Test batch query
for _, user := range users {
model := db.Model(TestTableName).
Sharding(shardingConfig).
ShardingValue(user.Id).
Safe()
var result ShardingUser
err := model.Where("id", user.Id).Scan(&result)
t.AssertNil(err)
t.Assert(result.Id, user.Id)
t.Assert(result.Name, user.Name)
}
// Clean up
for _, user := range users {
model := db.Model(TestTableName).
Sharding(shardingConfig).
ShardingValue(user.Id).
Safe()
_, err := model.Where("id", user.Id).Delete()
t.AssertNil(err)
}
})
}
func Test_Model_Sharding_Table_Using_Hook(t *testing.T) {
var (
table1 = gtime.TimestampNanoStr() + "_table1"
table2 = gtime.TimestampNanoStr() + "_table2"
)
createTable(table1)
defer dropTable(table1)
createTable(table2)
defer dropTable(table2)
shardingModel := db.Model(table1).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
in.Table = table2
return in.Next(ctx)
},
Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
in.Table = table2
return in.Next(ctx)
},
Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
in.Table = table2
return in.Next(ctx)
},
Delete: func(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) {
in.Table = table2
return in.Next(ctx)
},
})
gtest.C(t, func(t *gtest.T) {
r, err := shardingModel.Insert(g.Map{
"id": 1,
"passport": fmt.Sprintf(`user_%d`, 1),
"password": fmt.Sprintf(`pass_%d`, 1),
"nickname": fmt.Sprintf(`name_%d`, 1),
"create_time": gtime.NewFromStr(CreateTime).String(),
})
t.AssertNil(err)
n, err := r.RowsAffected()
t.AssertNil(err)
t.Assert(n, 1)
var count int
count, err = shardingModel.Count()
t.AssertNil(err)
t.Assert(count, 1)
count, err = db.Model(table1).Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db.Model(table2).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := shardingModel.Where(g.Map{
"id": 1,
}).Data(g.Map{
"passport": fmt.Sprintf(`user_%d`, 2),
"password": fmt.Sprintf(`pass_%d`, 2),
"nickname": fmt.Sprintf(`name_%d`, 2),
}).Update()
t.AssertNil(err)
n, err := r.RowsAffected()
t.AssertNil(err)
t.Assert(n, 1)
var (
count int
where = g.Map{"passport": fmt.Sprintf(`user_%d`, 2)}
)
count, err = shardingModel.Where(where).Count()
t.AssertNil(err)
t.Assert(count, 1)
count, err = db.Model(table1).Where(where).Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db.Model(table2).Where(where).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := shardingModel.Where(g.Map{
"id": 1,
}).Delete()
t.AssertNil(err)
n, err := r.RowsAffected()
t.AssertNil(err)
t.Assert(n, 1)
var count int
count, err = shardingModel.Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db.Model(table1).Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db.Model(table2).Count()
t.AssertNil(err)
t.Assert(count, 0)
})
}
func Test_Model_Sharding_Schema_Using_Hook(t *testing.T) {
var (
table = gtime.TimestampNanoStr() + "_table"
)
createTableWithDb(db, table)
defer dropTableWithDb(db, table)
createTableWithDb(db2, table)
defer dropTableWithDb(db2, table)
shardingModel := db.Model(table).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
in.Table = table
in.Schema = db2.GetSchema()
return in.Next(ctx)
},
Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
in.Table = table
in.Schema = db2.GetSchema()
return in.Next(ctx)
},
Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
in.Table = table
in.Schema = db2.GetSchema()
return in.Next(ctx)
},
Delete: func(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) {
in.Table = table
in.Schema = db2.GetSchema()
return in.Next(ctx)
},
})
gtest.C(t, func(t *gtest.T) {
r, err := shardingModel.Insert(g.Map{
"id": 1,
"passport": fmt.Sprintf(`user_%d`, 1),
"password": fmt.Sprintf(`pass_%d`, 1),
"nickname": fmt.Sprintf(`name_%d`, 1),
"create_time": gtime.NewFromStr(CreateTime).String(),
})
t.AssertNil(err)
n, err := r.RowsAffected()
t.AssertNil(err)
t.Assert(n, 1)
var count int
count, err = shardingModel.Count()
t.AssertNil(err)
t.Assert(count, 1)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db2.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := shardingModel.Where(g.Map{
"id": 1,
}).Data(g.Map{
"passport": fmt.Sprintf(`user_%d`, 2),
"password": fmt.Sprintf(`pass_%d`, 2),
"nickname": fmt.Sprintf(`name_%d`, 2),
}).Update()
t.AssertNil(err)
n, err := r.RowsAffected()
t.AssertNil(err)
t.Assert(n, 1)
var (
count int
where = g.Map{"passport": fmt.Sprintf(`user_%d`, 2)}
)
count, err = shardingModel.Where(where).Count()
t.AssertNil(err)
t.Assert(count, 1)
count, err = db.Model(table).Where(where).Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db2.Model(table).Where(where).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := shardingModel.Where(g.Map{
"id": 1,
}).Delete()
t.AssertNil(err)
n, err := r.RowsAffected()
t.AssertNil(err)
t.Assert(n, 1)
var count int
count, err = shardingModel.Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 0)
count, err = db2.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 0)
})
}

View File

@ -0,0 +1,545 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb_test
import (
"testing"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/gconv"
)
// Test_Model_AllAndCount_Basic tests basic AllAndCount functionality
func Test_Model_AllAndCount_Basic(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), TableSize)
t.Assert(count, TableSize)
})
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).AllAndCount(true)
t.AssertNil(err)
t.Assert(len(result), TableSize)
t.Assert(count, TableSize)
})
}
// Test_Model_AllAndCount_WithWhere tests AllAndCount with WHERE conditions
func Test_Model_AllAndCount_WithWhere(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Where("id > ?", 5).AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), 5)
t.Assert(count, 5)
t.Assert(result[0]["id"], 6)
})
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Where("id", g.Slice{1, 2, 3}).AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(count, 3)
})
}
// Test_Model_AllAndCount_WithPage tests AllAndCount with pagination
func Test_Model_AllAndCount_WithPage(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Page(1, 3).Order("id").AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(count, TableSize) // Count should be total, not page size
t.Assert(result[0]["id"], 1)
t.Assert(result[2]["id"], 3)
})
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Page(2, 3).Order("id").AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(count, TableSize)
t.Assert(result[0]["id"], 4)
})
}
// Test_Model_AllAndCount_WithFields tests AllAndCount with specific fields
// Related: https://github.com/gogf/gf/issues/4698
func Test_Model_AllAndCount_WithFields(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), TableSize)
t.Assert(count, TableSize)
t.Assert(len(result[0]), 2) // Only 2 fields
})
// Regression test for #4698: AllAndCount(true) with multiple fields should work correctly
// https://github.com/gogf/gf/issues/4698
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(true)
t.AssertNil(err)
t.Assert(len(result), TableSize)
t.Assert(count, TableSize)
t.Assert(len(result[0]), 2)
})
}
// Test_Model_AllAndCount_Empty tests AllAndCount with no results
func Test_Model_AllAndCount_Empty(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Where("id > ?", 1000).AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result), 0)
t.Assert(count, 0)
})
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Where("id < ?", 0).AllAndCount(true)
t.AssertNil(err)
t.Assert(len(result), 0)
t.Assert(count, 0)
})
}
// Test_Model_AllAndCount_WithCache tests AllAndCount with cache
func Test_Model_AllAndCount_WithCache(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result1, count1, err := db.Model(table).PageCache(gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}, gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}).Page(1, 5).AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result1), 5)
t.Assert(count1, TableSize)
// Second call should use cache
result2, count2, err := db.Model(table).PageCache(gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}, gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}).Page(1, 5).AllAndCount(false)
t.AssertNil(err)
t.Assert(len(result2), 5)
t.Assert(count2, count1)
})
}
// Test_Model_AllAndCount_Distinct tests AllAndCount with DISTINCT
func Test_Model_AllAndCount_Distinct(t *testing.T) {
table := createTable()
defer dropTable(table)
// Insert duplicate nicknames
for i := 1; i <= 10; i++ {
nickname := "name_" + gconv.String((i-1)/2) // Creates duplicates
db.Model(table).Data(g.Map{
"id": i,
"passport": "pass_" + gconv.String(i),
"password": "pwd",
"nickname": nickname,
}).Insert()
}
gtest.C(t, func(t *gtest.T) {
result, count, err := db.Model(table).Fields("DISTINCT nickname").AllAndCount(true)
t.AssertNil(err)
t.Assert(count, 5) // 10 records / 2 = 5 distinct nicknames
t.Assert(len(result), 5)
})
}
// Test_Model_ScanAndCount_Basic tests basic ScanAndCount functionality
func Test_Model_ScanAndCount_Basic(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Passport string
Password string
Nickname string
}
gtest.C(t, func(t *gtest.T) {
var users []User
var count int
err := db.Model(table).ScanAndCount(&users, &count, false)
t.AssertNil(err)
t.Assert(len(users), TableSize)
t.Assert(count, TableSize)
})
gtest.C(t, func(t *gtest.T) {
var users []User
var count int
err := db.Model(table).ScanAndCount(&users, &count, true)
t.AssertNil(err)
t.Assert(len(users), TableSize)
t.Assert(count, TableSize)
})
}
// Test_Model_ScanAndCount_WithWhere tests ScanAndCount with WHERE conditions
func Test_Model_ScanAndCount_WithWhere(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Passport string
Nickname string
}
gtest.C(t, func(t *gtest.T) {
var users []User
var count int
err := db.Model(table).Where("id <= ?", 5).ScanAndCount(&users, &count, false)
t.AssertNil(err)
t.Assert(len(users), 5)
t.Assert(count, 5)
t.Assert(users[0].Id, 1)
})
}
// Test_Model_ScanAndCount_WithPage tests ScanAndCount with pagination
func Test_Model_ScanAndCount_WithPage(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Nickname string
}
gtest.C(t, func(t *gtest.T) {
var users []User
var count int
err := db.Model(table).Page(2, 3).Order("id").ScanAndCount(&users, &count, false)
t.AssertNil(err)
t.Assert(len(users), 3)
t.Assert(count, TableSize) // Total count, not page count
t.Assert(users[0].Id, 4)
t.Assert(users[2].Id, 6)
})
}
// Test_Model_ScanAndCount_Single tests ScanAndCount for single record
func Test_Model_ScanAndCount_Single(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Passport string
}
gtest.C(t, func(t *gtest.T) {
var user User
var count int
err := db.Model(table).Where("id", 1).ScanAndCount(&user, &count, false)
t.AssertNil(err)
t.Assert(user.Id, 1)
t.Assert(count, 1)
})
}
// Test_Model_ScanAndCount_Empty tests ScanAndCount with no results
func Test_Model_ScanAndCount_Empty(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
}
gtest.C(t, func(t *gtest.T) {
var users []User
var count int
err := db.Model(table).Where("id > ?", 1000).ScanAndCount(&users, &count, false)
t.AssertNil(err)
t.Assert(len(users), 0)
t.Assert(count, 0)
})
}
// Test_Model_ScanAndCount_WithFields tests ScanAndCount with specific fields
func Test_Model_ScanAndCount_WithFields(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Nickname string
}
gtest.C(t, func(t *gtest.T) {
var users []User
var count int
err := db.Model(table).Fields("id, nickname").ScanAndCount(&users, &count, false)
t.AssertNil(err)
t.Assert(len(users), TableSize)
t.Assert(count, TableSize)
t.Assert(users[0].Id > 0, true)
t.AssertNE(users[0].Nickname, "")
})
}
// Test_Model_ScanAndCount_WithCache tests ScanAndCount with cache
func Test_Model_ScanAndCount_WithCache(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
}
gtest.C(t, func(t *gtest.T) {
var users1 []User
var count1 int
err := db.Model(table).PageCache(gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}, gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}).Page(1, 5).ScanAndCount(&users1, &count1, false)
t.AssertNil(err)
t.Assert(len(users1), 5)
t.Assert(count1, TableSize)
// Second call should use cache
var users2 []User
var count2 int
err = db.Model(table).PageCache(gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}, gdb.CacheOption{
Duration: time.Second * 10,
Force: false,
}).Page(1, 5).ScanAndCount(&users2, &count2, false)
t.AssertNil(err)
t.Assert(len(users2), 5)
t.Assert(count2, count1)
})
}
// Test_Model_Chunk_Basic tests basic Chunk functionality
func Test_Model_Chunk_Basic(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var (
total int
chunks int
)
db.Model(table).Order("id").Chunk(3, func(result gdb.Result, err error) bool {
t.AssertNil(err)
chunks++
total += len(result)
return true
})
t.Assert(chunks, 4) // 10 records / 3 = 4 chunks (3+3+3+1)
t.Assert(total, TableSize)
})
}
// Test_Model_Chunk_StopEarly tests Chunk with early stop
func Test_Model_Chunk_StopEarly(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var chunks int
db.Model(table).Order("id").Chunk(3, func(result gdb.Result, err error) bool {
t.AssertNil(err)
chunks++
return chunks < 2 // Stop after 2nd chunk
})
t.Assert(chunks, 2)
})
}
// Test_Model_Chunk_WithWhere tests Chunk with WHERE conditions
func Test_Model_Chunk_WithWhere(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var (
total int
chunks int
)
db.Model(table).Where("id <= ?", 5).Order("id").Chunk(2, func(result gdb.Result, err error) bool {
t.AssertNil(err)
chunks++
total += len(result)
return true
})
t.Assert(chunks, 3) // 5 records / 2 = 3 chunks (2+2+1)
t.Assert(total, 5)
})
}
// Test_Model_Chunk_ErrorHandling tests Chunk error handling
func Test_Model_Chunk_ErrorHandling(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var errorReceived bool
db.Model("non_existent_table").Chunk(10, func(result gdb.Result, err error) bool {
if err != nil {
errorReceived = true
return false
}
return true
})
t.Assert(errorReceived, true)
})
}
// Test_Model_Chunk_Empty tests Chunk with no results
func Test_Model_Chunk_Empty(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var chunks int
db.Model(table).Where("id > ?", 1000).Chunk(10, func(result gdb.Result, err error) bool {
chunks++
return true
})
t.Assert(chunks, 0) // No chunks for empty result
})
}
// Test_Model_Page_Boundary tests Page with boundary values
// Related: https://github.com/gogf/gf/issues/4699
func Test_Model_Page_Boundary(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// Page 0 should be treated as page 1
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Page(0, 3).Order("id").All()
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"], 1)
})
// Negative page should be treated as page 1
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Page(-1, 3).Order("id").All()
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"], 1)
})
// Size 0: framework treats limit=0 as "no limit", returns all records
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Page(1, 0).All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
// Negative size: normalized to 0, same as Page(1, 0)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Page(1, -1).All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
// Very large page number (beyond available data)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Page(100, 3).All()
t.AssertNil(err)
t.Assert(len(result), 0)
})
}
// Test_Model_Limit_Boundary tests Limit with boundary values
// Related: https://github.com/gogf/gf/issues/4699
func Test_Model_Limit_Boundary(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// Limit 0: framework treats limit=0 as "no limit", returns all records
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Limit(0).All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
// Negative limit: normalized to 0, same as Limit(0)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Limit(-1).All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
// Limit larger than available data
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Limit(1000).All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
// Limit(offset, size): offset=5 skips 5 rows, size=100 takes up to 100
// With 10 rows total, skipping 5 returns remaining 5 rows
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Limit(5, 100).All()
t.AssertNil(err)
t.Assert(len(result), TableSize-5)
})
// Offset beyond data: returns empty result
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Limit(100, 5).All()
t.AssertNil(err)
t.Assert(len(result), 0)
})
}
// Test_Model_Page_Limit_Combination tests Page and Limit used together
func Test_Model_Page_Limit_Combination(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Page should override Limit
result, err := db.Model(table).Limit(5).Page(1, 3).Order("id").All()
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"], 1)
})
}

View File

@ -0,0 +1,905 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Raw_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"id": gdb.Raw("id+2"),
"passport": "port_1",
"password": "pass_1",
"nickname": "name_1",
"create_time": gdb.Raw("now()"),
}).Insert()
t.AssertNil(err)
n, _ := result.LastInsertId()
t.Assert(n, 2)
})
}
func Test_Raw_BatchInsert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(
g.List{
g.Map{
"id": gdb.Raw("id+2"),
"passport": "port_2",
"password": "pass_2",
"nickname": "name_2",
"create_time": gdb.Raw("now()"),
},
g.Map{
"id": gdb.Raw("id+4"),
"passport": "port_4",
"password": "pass_4",
"nickname": "name_4",
"create_time": gdb.Raw("now()"),
},
},
).Insert()
t.AssertNil(err)
n, _ := result.LastInsertId()
t.Assert(n, 4)
})
}
func Test_Raw_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"id": gdb.Raw("id+100"),
"create_time": gdb.Raw("now()"),
}).Where("id", 1).Update()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
n, err := user.Where("id", 101).Count()
t.AssertNil(err)
t.Assert(n, 1)
})
}
func Test_Raw_Where(t *testing.T) {
table1 := createTable("Test_Raw_Where_Table1")
table2 := createTable("Test_Raw_Where_Table2")
defer dropTable(table1)
defer dropTable(table2)
// https://github.com/gogf/gf/issues/3922
gtest.C(t, func(t *gtest.T) {
expectSql := "SELECT * FROM `Test_Raw_Where_Table1` AS A WHERE NOT EXISTS (SELECT B.id FROM `Test_Raw_Where_Table2` AS B WHERE `B`.`id`=A.id) LIMIT 1"
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
s := db.Model(table2).As("B").Ctx(ctx).Fields("B.id").Where("B.id", gdb.Raw("A.id"))
m := db.Model(table1).As("A").Ctx(ctx).Where("NOT EXISTS ?", s).Limit(1)
_, err := m.All()
return err
})
t.AssertNil(err)
t.Assert(expectSql, sql)
})
gtest.C(t, func(t *gtest.T) {
expectSql := "SELECT * FROM `Test_Raw_Where_Table1` AS A WHERE NOT EXISTS (SELECT B.id FROM `Test_Raw_Where_Table2` AS B WHERE B.id=A.id) LIMIT 1"
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
s := db.Model(table2).As("B").Ctx(ctx).Fields("B.id").Where(gdb.Raw("B.id=A.id"))
m := db.Model(table1).As("A").Ctx(ctx).Where("NOT EXISTS ?", s).Limit(1)
_, err := m.All()
return err
})
t.AssertNil(err)
t.Assert(expectSql, sql)
})
// https://github.com/gogf/gf/issues/3915
gtest.C(t, func(t *gtest.T) {
expectSql := "SELECT * FROM `Test_Raw_Where_Table1` WHERE `passport` < `nickname`"
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
m := db.Model(table1).Ctx(ctx).WhereLT("passport", gdb.Raw("`nickname`"))
_, err := m.All()
return err
})
t.AssertNil(err)
t.Assert(expectSql, sql)
})
}
// Test_DataType_JSON_Insert tests JSON data insertion
func Test_DataType_JSON_Insert(t *testing.T) {
table := "test_json_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert simple JSON object
result, err := db.Model(table).Data(g.Map{
"data": `{"name":"John","age":30}`,
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
// Verify data
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
expected := map[string]interface{}{"name": "John", "age": float64(30)}
var actual map[string]interface{}
err = json.Unmarshal([]byte(one["data"].String()), &actual)
t.AssertNil(err)
t.Assert(actual, expected)
})
}
// Test_DataType_JSON_Extract tests JSON_EXTRACT function
func Test_DataType_JSON_Extract(t *testing.T) {
table := "test_json_extract_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert test data
_, err := db.Model(table).Data(g.Map{
"data": `{"name":"Alice","age":25,"city":"Beijing"}`,
}).Insert()
t.AssertNil(err)
// Extract name using JSON_EXTRACT
one, err := db.Model(table).Fields("JSON_EXTRACT(data, '$.name') as name").Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["name"].String(), `"Alice"`)
// Extract age
one, err = db.Model(table).Fields("JSON_EXTRACT(data, '$.age') as age").Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["age"].Int(), 25)
})
}
// Test_DataType_JSON_Set tests JSON_SET function
func Test_DataType_JSON_Set(t *testing.T) {
table := "test_json_set_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"data": `{"name":"Bob"}`,
}).Insert()
t.AssertNil(err)
// Update using JSON_SET
_, err = db.Exec(ctx, fmt.Sprintf("UPDATE %s SET data = JSON_SET(data, '$.age', 30) WHERE id = 1", table))
t.AssertNil(err)
// Verify updated data
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
expected := map[string]interface{}{"name": "Bob", "age": float64(30)}
var actual map[string]interface{}
err = json.Unmarshal([]byte(one["data"].String()), &actual)
t.AssertNil(err)
t.Assert(actual, expected)
})
}
// Test_DataType_JSON_Array tests JSON array operations
func Test_DataType_JSON_Array(t *testing.T) {
table := "test_json_array_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert JSON array
_, err := db.Model(table).Data(g.Map{
"data": `["apple","banana","cherry"]`,
}).Insert()
t.AssertNil(err)
// Extract array element
one, err := db.Model(table).Fields("JSON_EXTRACT(data, '$[0]') as first").Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["first"].String(), `"apple"`)
})
}
// Test_DataType_JSON_Null tests JSON NULL handling
func Test_DataType_JSON_Null(t *testing.T) {
table := "test_json_null_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert NULL value
_, err := db.Model(table).Data(g.Map{
"data": nil,
}).Insert()
t.AssertNil(err)
// Verify NULL
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["data"].IsNil(), true)
})
}
// Test_DataType_JSON_Complex tests complex nested JSON
func Test_DataType_JSON_Complex(t *testing.T) {
table := "test_json_complex_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert complex nested JSON
complexJSON := `{
"user": {
"name": "Charlie",
"contacts": {
"email": "charlie@example.com",
"phone": "1234567890"
},
"tags": ["developer", "gopher"]
}
}`
_, err := db.Model(table).Data(g.Map{
"data": complexJSON,
}).Insert()
t.AssertNil(err)
// Extract nested field
one, err := db.Model(table).Fields("JSON_EXTRACT(data, '$.user.contacts.email') as email").Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["email"].String(), `"charlie@example.com"`)
})
}
// Test_DataType_JSON_Query tests JSON query with WHERE clause
func Test_DataType_JSON_Query(t *testing.T) {
table := "test_json_query_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert multiple JSON records
_, err := db.Model(table).Data(g.List{
g.Map{"data": `{"name":"David","age":20}`},
g.Map{"data": `{"name":"Eve","age":30}`},
g.Map{"data": `{"name":"Frank","age":25}`},
}).Insert()
t.AssertNil(err)
// Query by JSON field value
count, err := db.Model(table).Where("JSON_EXTRACT(data, '$.age') > ?", 25).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}
// Test_DataType_JSON_Update tests updating JSON data
func Test_DataType_JSON_Update(t *testing.T) {
table := "test_json_update_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"data": `{"name":"Grace","age":28}`,
}).Insert()
t.AssertNil(err)
// Update entire JSON
_, err = db.Model(table).Data(g.Map{
"data": `{"name":"Grace","age":29}`,
}).Where("id", 1).Update()
t.AssertNil(err)
// Verify update
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
expected := map[string]interface{}{"name": "Grace", "age": float64(29)}
var actual map[string]interface{}
err = json.Unmarshal([]byte(one["data"].String()), &actual)
t.AssertNil(err)
t.Assert(actual, expected)
})
}
// Test_DataType_Binary_Small tests small binary data
func Test_DataType_Binary_Small(t *testing.T) {
table := "test_binary_small_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data BLOB)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert small binary data
binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF}
_, err := db.Model(table).Data(g.Map{
"data": binaryData,
}).Insert()
t.AssertNil(err)
// Verify data
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(bytes.Equal(one["data"].Bytes(), binaryData), true)
})
}
// Test_DataType_Binary_Large tests large binary data (1MB+)
func Test_DataType_Binary_Large(t *testing.T) {
table := "test_binary_large_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data MEDIUMBLOB)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Create 1MB binary data
size := 1024 * 1024 // 1MB
largeBinary := make([]byte, size)
for i := 0; i < size; i++ {
largeBinary[i] = byte(i % 256)
}
// Insert large binary data
_, err := db.Model(table).Data(g.Map{
"data": largeBinary,
}).Insert()
t.AssertNil(err)
// Verify data
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(len(one["data"].Bytes()), size)
t.Assert(bytes.Equal(one["data"].Bytes(), largeBinary), true)
})
}
// Test_DataType_Binary_Integrity tests binary data integrity with checksum
func Test_DataType_Binary_Integrity(t *testing.T) {
table := "test_binary_integrity_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data BLOB, checksum VARCHAR(64))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Create random binary data
binaryData := []byte("Hello, World! This is a binary test data with special chars: \x00\xFF\xAB")
// Calculate SHA256 checksum
hash := sha256.Sum256(binaryData)
checksum := hex.EncodeToString(hash[:])
// Insert with checksum
_, err := db.Model(table).Data(g.Map{
"data": binaryData,
"checksum": checksum,
}).Insert()
t.AssertNil(err)
// Verify integrity
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
retrievedHash := sha256.Sum256(one["data"].Bytes())
retrievedChecksum := hex.EncodeToString(retrievedHash[:])
t.Assert(retrievedChecksum, checksum)
})
}
// Test_DataType_Binary_Empty tests empty and NULL binary
func Test_DataType_Binary_Empty(t *testing.T) {
table := "test_binary_empty_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data BLOB)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert empty binary
_, err := db.Model(table).Data(g.Map{
"data": []byte{},
}).Insert()
t.AssertNil(err)
// Insert NULL
_, err = db.Model(table).Data(g.Map{
"data": nil,
}).Insert()
t.AssertNil(err)
// Verify empty
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(len(one["data"].Bytes()), 0)
// Verify NULL
one, err = db.Model(table).Where("id", 2).One()
t.AssertNil(err)
t.Assert(one["data"].IsNil(), true)
})
}
// Test_DataType_Decimal_HighPrecision tests high precision decimal (65,30)
func Test_DataType_Decimal_HighPrecision(t *testing.T) {
table := "test_decimal_precision_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, amount DECIMAL(65,30))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert high precision decimal
value := "12345678901234567890123456789012345.123456789012345678901234567890"
_, err := db.Model(table).Data(g.Map{
"amount": value,
}).Insert()
t.AssertNil(err)
// Verify precision
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["amount"].String(), value)
})
}
// Test_DataType_Decimal_Calculation tests decimal arithmetic
func Test_DataType_Decimal_Calculation(t *testing.T) {
table := "test_decimal_calc_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, price DECIMAL(10,2), quantity DECIMAL(10,2))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert test data
_, err := db.Model(table).Data(g.Map{
"price": "19.99",
"quantity": "3.5",
}).Insert()
t.AssertNil(err)
// Calculate total using SQL
one, err := db.Model(table).Fields("price * quantity as total").Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["total"].String(), "69.9650")
})
}
// Test_DataType_Decimal_Boundary tests decimal boundary values
func Test_DataType_Decimal_Boundary(t *testing.T) {
table := "test_decimal_boundary_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, value DECIMAL(10,2))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Test max value (10 digits, 2 decimals: 99999999.99)
_, err := db.Model(table).Data(g.Map{
"value": "99999999.99",
}).Insert()
t.AssertNil(err)
// Test min value
_, err = db.Model(table).Data(g.Map{
"value": "-99999999.99",
}).Insert()
t.AssertNil(err)
// Test zero
_, err = db.Model(table).Data(g.Map{
"value": "0.00",
}).Insert()
t.AssertNil(err)
// Verify all values
all, err := db.Model(table).Order("id").All()
t.AssertNil(err)
t.Assert(len(all), 3)
t.Assert(all[0]["value"].String(), "99999999.99")
t.Assert(all[1]["value"].String(), "-99999999.99")
t.Assert(all[2]["value"].String(), "0.00")
})
}
// Test_DataType_Decimal_Null tests NULL decimal values
func Test_DataType_Decimal_Null(t *testing.T) {
table := "test_decimal_null_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, value DECIMAL(10,2))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert NULL
_, err := db.Model(table).Data(g.Map{
"value": nil,
}).Insert()
t.AssertNil(err)
// Verify NULL
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["value"].IsNil(), true)
})
}
// Test_DataType_Datetime_Timezone tests datetime with timezone handling
func Test_DataType_Datetime_Timezone(t *testing.T) {
table := "test_datetime_tz_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, created_at DATETIME)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert datetime
dt := "2024-01-15 12:30:45"
_, err := db.Model(table).Data(g.Map{
"created_at": dt,
}).Insert()
t.AssertNil(err)
// Verify datetime
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["created_at"].String(), dt)
})
}
// Test_DataType_Datetime_Precision tests datetime with microsecond precision
func Test_DataType_Datetime_Precision(t *testing.T) {
table := "test_datetime_precision_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, created_at DATETIME(6))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert datetime with microseconds
dt := "2024-01-15 12:30:45.123456"
_, err := db.Model(table).Data(g.Map{
"created_at": dt,
}).Insert()
t.AssertNil(err)
// Verify precision (compare up to seconds, MySQL may format microseconds differently)
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
expected := "2024-01-15 12:30:45"
actual := one["created_at"].String()[:19] // Extract first 19 chars (YYYY-MM-DD HH:MM:SS)
t.Assert(actual, expected)
})
}
// Test_DataType_Datetime_Boundary tests datetime boundary values
func Test_DataType_Datetime_Boundary(t *testing.T) {
table := "test_datetime_boundary_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, dt DATETIME)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Test min datetime (MySQL supports 1000-01-01 00:00:00)
_, err := db.Model(table).Data(g.Map{
"dt": "1000-01-01 00:00:00",
}).Insert()
t.AssertNil(err)
// Test max datetime
_, err = db.Model(table).Data(g.Map{
"dt": "9999-12-31 23:59:59",
}).Insert()
t.AssertNil(err)
// Verify boundaries
all, err := db.Model(table).Order("id").All()
t.AssertNil(err)
t.Assert(len(all), 2)
t.Assert(all[0]["dt"].String(), "1000-01-01 00:00:00")
t.Assert(all[1]["dt"].String(), "9999-12-31 23:59:59")
})
}
// Test_DataType_Datetime_Null tests NULL datetime
func Test_DataType_Datetime_Null(t *testing.T) {
table := "test_datetime_null_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, dt DATETIME)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert NULL
_, err := db.Model(table).Data(g.Map{
"dt": nil,
}).Insert()
t.AssertNil(err)
// Verify NULL
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["dt"].IsNil(), true)
})
}
// Test_DataType_Datetime_Update tests datetime updates
func Test_DataType_Datetime_Update(t *testing.T) {
table := "test_datetime_update_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, dt DATETIME)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial datetime
dt1 := "2024-01-01 10:00:00"
_, err := db.Model(table).Data(g.Map{
"dt": dt1,
}).Insert()
t.AssertNil(err)
// Update datetime
dt2 := "2024-12-31 23:59:59"
_, err = db.Model(table).Data(g.Map{
"dt": dt2,
}).Where("id", 1).Update()
t.AssertNil(err)
// Verify update
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["dt"].String(), dt2)
})
}
// Test_DataType_Enum_Valid tests valid ENUM values
func Test_DataType_Enum_Valid(t *testing.T) {
table := "test_enum_valid_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, status ENUM('pending','approved','rejected'))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert all valid values
_, err := db.Model(table).Data(g.List{
g.Map{"status": "pending"},
g.Map{"status": "approved"},
g.Map{"status": "rejected"},
}).Insert()
t.AssertNil(err)
// Verify all values
all, err := db.Model(table).Order("id").All()
t.AssertNil(err)
t.Assert(len(all), 3)
t.Assert(all[0]["status"].String(), "pending")
t.Assert(all[1]["status"].String(), "approved")
t.Assert(all[2]["status"].String(), "rejected")
})
}
// Test_DataType_Enum_Invalid tests invalid ENUM values (should fail or truncate)
func Test_DataType_Enum_Invalid(t *testing.T) {
table := "test_enum_invalid_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, status ENUM('pending','approved','rejected'))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Attempt to insert invalid value (should fail in strict mode)
_, err := db.Model(table).Data(g.Map{
"status": "invalid_status",
}).Insert()
// In strict SQL mode, this should produce an error
// In non-strict mode, it might insert empty string
t.AssertNE(err, nil)
})
}
// Test_DataType_Set_Valid tests valid SET values
func Test_DataType_Set_Valid(t *testing.T) {
table := "test_set_valid_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, permissions SET('read','write','execute'))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert single value
_, err := db.Model(table).Data(g.Map{
"permissions": "read",
}).Insert()
t.AssertNil(err)
// Insert multiple values
_, err = db.Model(table).Data(g.Map{
"permissions": "read,write",
}).Insert()
t.AssertNil(err)
// Insert all values
_, err = db.Model(table).Data(g.Map{
"permissions": "read,write,execute",
}).Insert()
t.AssertNil(err)
// Verify all values
all, err := db.Model(table).Order("id").All()
t.AssertNil(err)
t.Assert(len(all), 3)
t.Assert(all[0]["permissions"].String(), "read")
t.Assert(all[1]["permissions"].String(), "read,write")
t.Assert(all[2]["permissions"].String(), "read,write,execute")
})
}
// Test_DataType_Set_Empty tests empty SET values
func Test_DataType_Set_Empty(t *testing.T) {
table := "test_set_empty_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, permissions SET('read','write','execute'))")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert empty SET
_, err := db.Model(table).Data(g.Map{
"permissions": "",
}).Insert()
t.AssertNil(err)
// Verify empty
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["permissions"].String(), "")
})
}
// Test_DataType_Geometry_Point tests POINT geometry type
func Test_DataType_Geometry_Point(t *testing.T) {
table := "test_geo_point_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, location POINT)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert POINT using ST_GeomFromText
_, err := db.Exec(ctx, fmt.Sprintf("INSERT INTO %s (location) VALUES (ST_GeomFromText('POINT(116.4074 39.9042)'))", table))
t.AssertNil(err)
// Query POINT using ST_AsText
one, err := db.Model(table).Fields("ST_AsText(location) as location_text").Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["location_text"].String(), "POINT(116.4074 39.9042)")
})
}
// Test_DataType_Geometry_Polygon tests POLYGON geometry type
func Test_DataType_Geometry_Polygon(t *testing.T) {
table := "test_geo_polygon_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, area POLYGON)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert POLYGON (rectangle)
polygon := "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"
_, err := db.Exec(ctx, fmt.Sprintf("INSERT INTO %s (area) VALUES (ST_GeomFromText('%s'))", table, polygon))
t.AssertNil(err)
// Query POLYGON (normalize spaces for comparison)
one, err := db.Model(table).Fields("ST_AsText(area) as area_text").Where("id", 1).One()
t.AssertNil(err)
expected := "POLYGON((0 0,10 0,10 10,0 10,0 0))"
actual := strings.ReplaceAll(one["area_text"].String(), ", ", ",") // Remove spaces after commas
t.Assert(actual, expected)
})
}
// Test_DataType_Geometry_Null tests NULL geometry values
func Test_DataType_Geometry_Null(t *testing.T) {
table := "test_geo_null_" + gtime.TimestampMicroStr()
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, location POINT)")
if err != nil {
t.Fatal(err)
}
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert NULL
_, err := db.Model(table).Data(g.Map{
"location": nil,
}).Insert()
t.AssertNil(err)
// Verify NULL
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["location"].IsNil(), true)
})
}