diff --git a/contrib/drivers/mariadb/mariadb_unit_init_test.go b/contrib/drivers/mariadb/mariadb_unit_init_test.go index 4d4e978a0..84b063708 100644 --- a/contrib/drivers/mariadb/mariadb_unit_init_test.go +++ b/contrib/drivers/mariadb/mariadb_unit_init_test.go @@ -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 { diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_concurrent_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_concurrent_test.go new file mode 100644 index 000000000..a03edfb2b --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_concurrent_test.go @@ -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) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_error_handling_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_error_handling_test.go new file mode 100644 index 000000000..7fb1f939e --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_error_handling_test.go @@ -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) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_model_sharding_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_model_sharding_test.go new file mode 100644 index 000000000..8dc5ac6f8 --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_model_sharding_test.go @@ -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) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_pagination_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_pagination_test.go new file mode 100644 index 000000000..68840efe7 --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_pagination_test.go @@ -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) + }) +} diff --git a/contrib/drivers/mariadb/mariadb_z_unit_feature_raw_type_test.go b/contrib/drivers/mariadb/mariadb_z_unit_feature_raw_type_test.go new file mode 100644 index 000000000..aae5ba1ba --- /dev/null +++ b/contrib/drivers/mariadb/mariadb_z_unit_feature_raw_type_test.go @@ -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) + }) +}