mirror of
https://gitee.com/johng/gf
synced 2026-07-06 21:45:34 +08:00
test(contrib/drivers/mysql): enhance transaction tests (#4704)
## Summary - Add nested transaction tests - Add transaction propagation tests - Add transaction rollback/commit behavior tests - Add transaction context handling tests **Test coverage added:** ~25 test functions Ref #4689 ## Test plan ```bash cd contrib/drivers/mysql go test -v -run "TestTX_Nested|TestTX_Propagation|TestTX_Transaction" ```
This commit is contained in:
@ -10,7 +10,9 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@ -18,6 +20,7 @@ import (
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
func Test_TX_Query(t *testing.T) {
|
||||
@ -1785,3 +1788,865 @@ func Test_Transaction_Spread(t *testing.T) {
|
||||
t.Assert(len(all), 0)
|
||||
})
|
||||
}
|
||||
|
||||
// ========== Deep Transaction Enhancement Tests ==========
|
||||
|
||||
// Test_Transaction_Isolation_ReadCommitted_NonRepeatableRead tests READ COMMITTED isolation level
|
||||
// allows non-repeatable reads - same query can return different results within the same transaction
|
||||
func Test_Transaction_Isolation_ReadCommitted_NonRepeatableRead(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
Isolation: sql.LevelReadCommitted,
|
||||
}, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
// First read
|
||||
v1, err := tx1.Model(table).Where("id=1").Value("passport")
|
||||
t.AssertNil(err)
|
||||
firstRead := v1.String()
|
||||
t.Assert(firstRead, "user_1")
|
||||
|
||||
// External transaction commits a change
|
||||
err = db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
}, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Update(table, g.Map{"passport": "user_1_modified"}, "id=1")
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Second read - should see the committed change (non-repeatable read)
|
||||
v2, err := tx1.Model(table).Where("id=1").Value("passport")
|
||||
t.AssertNil(err)
|
||||
secondRead := v2.String()
|
||||
t.Assert(secondRead, "user_1_modified")
|
||||
t.AssertNE(firstRead, secondRead)
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Isolation_Serializable_PhantomRead tests SERIALIZABLE isolation level
|
||||
// prevents phantom reads - range queries see consistent snapshot
|
||||
func Test_Transaction_Isolation_Serializable_PhantomRead(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
Isolation: sql.LevelSerializable,
|
||||
}, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
// First count
|
||||
count1, err := tx1.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count1, int64(TableSize))
|
||||
|
||||
// Try to insert in another transaction.
|
||||
// InnoDB's SERIALIZABLE uses gap locks; whether this insert conflicts
|
||||
// depends on table state and index structure, so we do not assert on err.
|
||||
_ = db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
Isolation: sql.LevelSerializable,
|
||||
}, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{
|
||||
"id": 100,
|
||||
"passport": "phantom_user",
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
// Second count - should remain the same
|
||||
count2, err := tx1.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count2, count1)
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Isolation_RepeatableRead_ConsistentSnapshot tests REPEATABLE READ isolation
|
||||
// maintains consistent snapshot throughout transaction
|
||||
func Test_Transaction_Isolation_RepeatableRead_ConsistentSnapshot(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
Isolation: sql.LevelRepeatableRead,
|
||||
}, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
// Read multiple records
|
||||
records1, err := tx1.Model(table).Where("id IN(?,?)", 1, 2).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(records1), 2)
|
||||
|
||||
// External transaction modifies both records
|
||||
err = db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
}, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Update(table, g.Map{"nickname": "modified"}, "id IN(?,?)", 1, 2)
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Re-read - should see original values
|
||||
records2, err := tx1.Model(table).Where("id IN(?,?)", 1, 2).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(records2), 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Assert(records1[i]["nickname"], records2[i]["nickname"])
|
||||
t.AssertNE(records2[i]["nickname"].String(), "modified")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Deadlock_TwoTables tests deadlock detection with two tables
|
||||
func Test_Transaction_Deadlock_TwoTables(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table1 := createInitTable()
|
||||
table2 := createInitTable()
|
||||
defer dropTable(table1)
|
||||
defer dropTable(table2)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, 2)
|
||||
// Use channels to synchronize lock acquisition order.
|
||||
tx1Locked := make(chan struct{})
|
||||
tx2Locked := make(chan struct{})
|
||||
|
||||
// Transaction 1: lock table1 then table2
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs[0] = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Update(table1, g.Map{"passport": "tx1_lock"}, "id=1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
close(tx1Locked)
|
||||
<-tx2Locked
|
||||
_, err = tx.Update(table2, g.Map{"passport": "tx1_lock"}, "id=1")
|
||||
return err
|
||||
})
|
||||
}()
|
||||
|
||||
// Transaction 2: lock table2 then table1
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs[1] = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
<-tx1Locked
|
||||
_, err := tx.Update(table2, g.Map{"passport": "tx2_lock"}, "id=1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
close(tx2Locked)
|
||||
_, err = tx.Update(table1, g.Map{"passport": "tx2_lock"}, "id=1")
|
||||
return err
|
||||
})
|
||||
}()
|
||||
|
||||
// Wait for both transactions to complete
|
||||
wg.Wait()
|
||||
|
||||
// At least one transaction should fail due to deadlock
|
||||
t.Assert(errs[0] != nil || errs[1] != nil, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Deadlock_SameTable tests deadlock detection on same table
|
||||
func Test_Transaction_Deadlock_SameTable(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, 2)
|
||||
// Use channels to synchronize lock acquisition order.
|
||||
tx1Locked := make(chan struct{})
|
||||
tx2Locked := make(chan struct{})
|
||||
|
||||
// Transaction 1: lock id=1 then id=2
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs[0] = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Update(table, g.Map{"nickname": "tx1"}, "id=1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
close(tx1Locked)
|
||||
<-tx2Locked
|
||||
_, err = tx.Update(table, g.Map{"nickname": "tx1"}, "id=2")
|
||||
return err
|
||||
})
|
||||
}()
|
||||
|
||||
// Transaction 2: lock id=2 then id=1
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs[1] = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
<-tx1Locked
|
||||
_, err := tx.Update(table, g.Map{"nickname": "tx2"}, "id=2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
close(tx2Locked)
|
||||
_, err = tx.Update(table, g.Map{"nickname": "tx2"}, "id=1")
|
||||
return err
|
||||
})
|
||||
}()
|
||||
|
||||
// Wait for both transactions to complete
|
||||
wg.Wait()
|
||||
|
||||
// At least one transaction should fail due to deadlock
|
||||
t.Assert(errs[0] != nil || errs[1] != nil, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Deadlock_Retry tests automatic retry on deadlock
|
||||
func Test_Transaction_Deadlock_Retry(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
maxRetries := 3
|
||||
var retryCount int
|
||||
|
||||
executeWithRetry := func(fn func(context.Context, gdb.TX) error) error {
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
err := db.Transaction(ctx, fn)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// Check if error message contains deadlock-related keywords.
|
||||
errMsg := err.Error()
|
||||
if gstr.ContainsI(errMsg, "deadlock") || gstr.ContainsI(errMsg, "lock wait timeout") {
|
||||
retryCount++
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return gerror.New("max retries exceeded")
|
||||
}
|
||||
|
||||
// A simple non-conflicting update should succeed on first attempt.
|
||||
err := executeWithRetry(func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Update(table, g.Map{"passport": "retry_test"}, "id=1")
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(retryCount, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Nested_7Levels tests 7-level deep nested transactions
|
||||
func Test_Transaction_Nested_7Levels(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
_, err := tx1.Insert(table, g.Map{"id": 1, "passport": "level1"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx1.Transaction(ctx, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{"id": 2, "passport": "level2"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx2.Transaction(ctx, func(ctx context.Context, tx3 gdb.TX) error {
|
||||
_, err := tx3.Insert(table, g.Map{"id": 3, "passport": "level3"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx3.Transaction(ctx, func(ctx context.Context, tx4 gdb.TX) error {
|
||||
_, err := tx4.Insert(table, g.Map{"id": 4, "passport": "level4"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx4.Transaction(ctx, func(ctx context.Context, tx5 gdb.TX) error {
|
||||
_, err := tx5.Insert(table, g.Map{"id": 5, "passport": "level5"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx5.Transaction(ctx, func(ctx context.Context, tx6 gdb.TX) error {
|
||||
_, err := tx6.Insert(table, g.Map{"id": 6, "passport": "level6"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx6.Transaction(ctx, func(ctx context.Context, tx7 gdb.TX) error {
|
||||
_, err := tx7.Insert(table, g.Map{"id": 7, "passport": "level7"})
|
||||
return err
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all records exist
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(7))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Nested_7Levels_PartialRollback tests partial rollback in deep nesting
|
||||
func Test_Transaction_Nested_7Levels_PartialRollback(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
_, err := tx1.Insert(table, g.Map{"id": 1, "passport": "level1"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx1.Transaction(ctx, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{"id": 2, "passport": "level2"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx2.Transaction(ctx, func(ctx context.Context, tx3 gdb.TX) error {
|
||||
_, err := tx3.Insert(table, g.Map{"id": 3, "passport": "level3"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx3.Transaction(ctx, func(ctx context.Context, tx4 gdb.TX) error {
|
||||
_, err := tx4.Insert(table, g.Map{"id": 4, "passport": "level4"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx4.Transaction(ctx, func(ctx context.Context, tx5 gdb.TX) error {
|
||||
_, err := tx5.Insert(table, g.Map{"id": 5, "passport": "level5"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx5.Transaction(ctx, func(ctx context.Context, tx6 gdb.TX) error {
|
||||
_, err := tx6.Insert(table, g.Map{"id": 6, "passport": "level6"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx6.Transaction(ctx, func(ctx context.Context, tx7 gdb.TX) error {
|
||||
_, err := tx7.Insert(table, g.Map{"id": 7, "passport": "level7"})
|
||||
t.AssertNil(err)
|
||||
// Fail at deepest level
|
||||
return gerror.New("rollback from level 7")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// Verify all records are rolled back
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(0))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Nested_10Levels tests maximum depth of 10 levels
|
||||
func Test_Transaction_Nested_10Levels(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
_, err := tx1.Insert(table, g.Map{"id": 1, "passport": "level1"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx1.Transaction(ctx, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{"id": 2, "passport": "level2"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx2.Transaction(ctx, func(ctx context.Context, tx3 gdb.TX) error {
|
||||
_, err := tx3.Insert(table, g.Map{"id": 3, "passport": "level3"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx3.Transaction(ctx, func(ctx context.Context, tx4 gdb.TX) error {
|
||||
_, err := tx4.Insert(table, g.Map{"id": 4, "passport": "level4"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx4.Transaction(ctx, func(ctx context.Context, tx5 gdb.TX) error {
|
||||
_, err := tx5.Insert(table, g.Map{"id": 5, "passport": "level5"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx5.Transaction(ctx, func(ctx context.Context, tx6 gdb.TX) error {
|
||||
_, err := tx6.Insert(table, g.Map{"id": 6, "passport": "level6"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx6.Transaction(ctx, func(ctx context.Context, tx7 gdb.TX) error {
|
||||
_, err := tx7.Insert(table, g.Map{"id": 7, "passport": "level7"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx7.Transaction(ctx, func(ctx context.Context, tx8 gdb.TX) error {
|
||||
_, err := tx8.Insert(table, g.Map{"id": 8, "passport": "level8"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx8.Transaction(ctx, func(ctx context.Context, tx9 gdb.TX) error {
|
||||
_, err := tx9.Insert(table, g.Map{"id": 9, "passport": "level9"})
|
||||
t.AssertNil(err)
|
||||
|
||||
return tx9.Transaction(ctx, func(ctx context.Context, tx10 gdb.TX) error {
|
||||
_, err := tx10.Insert(table, g.Map{"id": 10, "passport": "level10"})
|
||||
return err
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all records exist
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(10))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Nested_SavePoint_Multiple tests multiple savepoints in nested transactions
|
||||
func Test_Transaction_Nested_SavePoint_Multiple(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert and create first savepoint
|
||||
_, err = tx.Insert(table, g.Map{"id": 1, "passport": "sp1"})
|
||||
t.AssertNil(err)
|
||||
err = tx.SavePoint("sp1")
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert and create second savepoint
|
||||
_, err = tx.Insert(table, g.Map{"id": 2, "passport": "sp2"})
|
||||
t.AssertNil(err)
|
||||
err = tx.SavePoint("sp2")
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert and create third savepoint
|
||||
_, err = tx.Insert(table, g.Map{"id": 3, "passport": "sp3"})
|
||||
t.AssertNil(err)
|
||||
err = tx.SavePoint("sp3")
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert without savepoint
|
||||
_, err = tx.Insert(table, g.Map{"id": 4, "passport": "no_sp"})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Rollback to sp2
|
||||
err = tx.RollbackTo("sp2")
|
||||
t.AssertNil(err)
|
||||
|
||||
// Commit transaction
|
||||
err = tx.Commit()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify only records up to sp2 exist
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(2))
|
||||
|
||||
v1, err := db.Model(table).Where("id=1").Value("passport")
|
||||
t.AssertNil(err)
|
||||
t.Assert(v1.String(), "sp1")
|
||||
|
||||
v2, err := db.Model(table).Where("id=2").Value("passport")
|
||||
t.AssertNil(err)
|
||||
t.Assert(v2.String(), "sp2")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Nested_SavePoint_RollbackToNonExistent tests rollback to non-existent savepoint
|
||||
func Test_Transaction_Nested_SavePoint_RollbackToNonExistent(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try to rollback to non-existent savepoint
|
||||
err = tx.RollbackTo("non_existent")
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
err = tx.Rollback()
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Concurrent_Insert tests concurrent inserts in separate transactions
|
||||
func Test_Transaction_Concurrent_Insert(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
var wg = sync.WaitGroup{}
|
||||
concurrency := 10
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Insert(table, g.Map{
|
||||
"id": index + 1,
|
||||
"passport": fmt.Sprintf("user_%d", index+1),
|
||||
})
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify all records exist
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(concurrency))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Concurrent_Update tests concurrent updates to same record
|
||||
func Test_Transaction_Concurrent_Update(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
var wg = sync.WaitGroup{}
|
||||
concurrency := 5
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
_ = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Update(table, g.Map{
|
||||
"nickname": fmt.Sprintf("concurrent_%d", index),
|
||||
}, "id=1")
|
||||
return err
|
||||
})
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify record was updated (one of the concurrent values should win)
|
||||
v, err := db.Model(table).Where("id=1").Value("nickname")
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(v.String(), "name_1")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Mixed_Propagation_Nested tests mixed propagation modes in nested transactions
|
||||
func Test_Transaction_Mixed_Propagation_Nested(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx1 gdb.TX) error {
|
||||
_, err := tx1.Insert(table, g.Map{"id": 1, "passport": "outer"})
|
||||
t.AssertNil(err)
|
||||
|
||||
// REQUIRES_NEW - should create independent transaction
|
||||
err = tx1.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequiresNew,
|
||||
}, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{"id": 2, "passport": "independent"})
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// NESTED - should create savepoint
|
||||
err = tx1.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationNested,
|
||||
}, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{"id": 3, "passport": "nested"})
|
||||
t.AssertNil(err)
|
||||
return gerror.New("rollback nested")
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// REQUIRED - should use existing transaction
|
||||
err = tx1.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
Propagation: gdb.PropagationRequired,
|
||||
}, func(ctx context.Context, tx2 gdb.TX) error {
|
||||
_, err := tx2.Insert(table, g.Map{"id": 4, "passport": "required"})
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify results: outer, independent, and required should exist; nested should not
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(3))
|
||||
|
||||
exists, err := db.Model(table).Where("passport", "nested").Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(exists, int64(0))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Rollback_After_Commit tests that rollback after commit fails
|
||||
func Test_Transaction_Rollback_After_Commit(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNil(err)
|
||||
|
||||
err = tx.Commit()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try to rollback after commit
|
||||
err = tx.Rollback()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Commit_After_Rollback tests that commit after rollback fails
|
||||
func Test_Transaction_Commit_After_Rollback(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNil(err)
|
||||
|
||||
err = tx.Rollback()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try to commit after rollback
|
||||
err = tx.Commit()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Operation_After_Commit tests that operations after commit fail
|
||||
func Test_Transaction_Operation_After_Commit(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
err = tx.Commit()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try to insert after commit
|
||||
_, err = tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Operation_After_Rollback tests that operations after rollback fail
|
||||
func Test_Transaction_Operation_After_Rollback(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
err = tx.Rollback()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try to insert after rollback
|
||||
_, err = tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Context_Timeout tests transaction with context timeout
|
||||
func Test_Transaction_Context_Timeout(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*gtime.MS)
|
||||
defer cancel()
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Wait for context timeout instead of using fixed sleep.
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Context_Cancel tests transaction with context cancellation
|
||||
func Test_Transaction_Context_Cancel(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Insert(table, g.Map{"id": 1, "passport": "test"})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Wait for context cancellation instead of using fixed sleep.
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Empty_NoOperations tests empty transaction with no operations
|
||||
func Test_Transaction_Empty_NoOperations(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// No operations
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Large_Batch_Insert tests transaction with large batch insert
|
||||
func Test_Transaction_Large_Batch_Insert(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
batchSize := 1000
|
||||
data := make(g.List, batchSize)
|
||||
for i := 0; i < batchSize; i++ {
|
||||
data[i] = g.Map{
|
||||
"id": i + 1,
|
||||
"passport": fmt.Sprintf("user_%d", i+1),
|
||||
}
|
||||
}
|
||||
|
||||
_, err := tx.Insert(table, data)
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all records inserted
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(1000))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_Large_Batch_Update tests transaction with large batch update
|
||||
func Test_Transaction_Large_Batch_Update(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// First insert records
|
||||
batchSize := 500
|
||||
data := make(g.List, batchSize)
|
||||
for i := 0; i < batchSize; i++ {
|
||||
data[i] = g.Map{
|
||||
"id": i + 1,
|
||||
"passport": fmt.Sprintf("user_%d", i+1),
|
||||
}
|
||||
}
|
||||
_, err := db.Insert(ctx, table, data)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Update all records in transaction (WHERE required for safety)
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Model(table).Where("id > ?", 0).Update(g.Map{"nickname": "updated"})
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all records updated
|
||||
count, err := db.Model(table).Where("nickname", "updated").Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(batchSize))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_ReadOnly_WithUpdate tests that updates fail in read-only transactions
|
||||
func Test_Transaction_ReadOnly_WithUpdate(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
ReadOnly: true,
|
||||
}, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Read operations should work
|
||||
_, err := tx.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Write operations should fail
|
||||
_, err = tx.Insert(table, g.Map{
|
||||
"id": 100,
|
||||
"passport": "new_user",
|
||||
})
|
||||
return err
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Transaction_ReadOnly_WithDelete tests that deletes fail in read-only transactions
|
||||
func Test_Transaction_ReadOnly_WithDelete(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
err := db.TransactionWithOptions(ctx, gdb.TxOptions{
|
||||
ReadOnly: true,
|
||||
}, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Delete(table, "id=1")
|
||||
return err
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// Verify record still exists
|
||||
count, err := db.Model(table).Where("id=1").Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(1))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user