From c86b7cc8cdd382ffac1e2d5fb9de74c9bffd6f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Dulai?= Date: Sun, 24 May 2026 18:30:00 +0200 Subject: [PATCH] WIP: Transaction implementation --- exec/exec.go | 71 ++++++++++---- test/exec_test.go | 202 ++++++++++++++++++++++++++-------------- test/main_test.go | 17 ++++ test/repository_test.go | 22 +---- 4 files changed, 207 insertions(+), 105 deletions(-) diff --git a/exec/exec.go b/exec/exec.go index 0386a47..3d46575 100644 --- a/exec/exec.go +++ b/exec/exec.go @@ -10,6 +10,7 @@ import ( log "gitlab.com/gdulai/simpleloglvl" ) +// Created and executes the DDL created by [simpleorm.ORM] func ExecuteDDL(conn *simpleorm.DBConnection, orm *simpleorm.ORM) { schema, err := orm.CreateSchema() if err != nil { @@ -24,6 +25,8 @@ func ExecuteDDL(conn *simpleorm.DBConnection, orm *simpleorm.ORM) { } } +// Wraps and represents a DB transaction. +// Allows for all at once or separate execution of [exec.Exec] implementations. type Transaction struct { conn *simpleorm.DBConnection executions []*Exec[any] @@ -31,14 +34,20 @@ type Transaction struct { finished bool } -func (t *Transaction) NewTransaction(conn *simpleorm.DBConnection, executions ...*Exec[any]) *Transaction { +// Creates a new [exec.Transaction]. +// Can be initialized with a set of [exec.Exec]s. +func NewTransaction(conn *simpleorm.DBConnection, executions ...*Exec[any]) *Transaction { return &Transaction{conn: conn, executions: executions, finished: false} } +// The executions assigned to this transaction. func (t *Transaction) Executions() []*Exec[any] { return t.executions } +// Executes all the [exec.Exec] implementations assigned to this transactions. +// Begins, commits or rollbacks the transaction. +// This is a terminal operation, the [exec.Transaction] is considered finished after calling this. func (t *Transaction) ExecuteAtOnce() error { if t.finished { return errors.New("Transaction already finished.") @@ -56,11 +65,10 @@ func (t *Transaction) ExecuteAtOnce() error { for _, exec := range t.executions { err := (*exec).execute(nil, t.tx) if err != nil { - rollbackErr := t.tx.Rollback() + rollbackErr := t.Rollback() if rollbackErr != nil { return rollbackErr } - t.finished = true return err } @@ -75,34 +83,42 @@ func (t *Transaction) ExecuteAtOnce() error { return nil } -func (t *Transaction) Execute(exec *Exec[any]) error { +// Executes and assigns the passed [exec.Exec]s to the Transaction. +// Calls rollback in case of an error. +// This is NOT a terminal operation, transactions still has to be committed. +func (t *Transaction) Execute(execs ...Exec[any]) error { if t.finished { return errors.New("Transaction already finished.") } - t.executions = append(t.executions, exec) - if t.tx == nil { - tx, err := t.conn.Begin() - t.tx = tx + for _, exec := range execs { + execPtr := &exec + t.executions = append(t.executions, execPtr) + if t.tx == nil { + tx, err := t.conn.Begin() + t.tx = tx + if err != nil { + t.finished = true + return err + } + } + + err := (*execPtr).execute(nil, t.tx) if err != nil { - t.finished = true + rollbackErr := t.Rollback() + if rollbackErr != nil { + return rollbackErr + } return err } - } - err := (*exec).execute(nil, t.tx) - if err != nil { - rollbackErr := t.tx.Rollback() - if rollbackErr != nil { - return rollbackErr - } - t.finished = true - return err } return nil } +// Finishes the transaction, commits the changes. +// This is terminal operation. func (t *Transaction) Finish() error { if t.finished { return errors.New("Transaction already finished.") @@ -117,6 +133,25 @@ func (t *Transaction) Finish() error { return nil } +// Rollbacks and aborts the transaction. +// This is a terminal operation. +func (t *Transaction) Rollback() error { + if t.finished { + return errors.New("Transaction already finished.") + } + + if t.tx == nil { + return errors.New("Transaction is nil.") + } + + rollbackErr := t.tx.Rollback() + if rollbackErr != nil { + return rollbackErr + } + t.finished = true + return nil +} + type Exec[T any] interface { Execute(conn *simpleorm.DBConnection) error execute(conn *simpleorm.DBConnection, tx *sql.Tx) error diff --git a/test/exec_test.go b/test/exec_test.go index 08a988c..76d1b31 100644 --- a/test/exec_test.go +++ b/test/exec_test.go @@ -1,35 +1,22 @@ package simpleorm_test import ( - "os" "testing" "time" - simpleorm "git.gdulai.com/gdulai/simpleorm" "git.gdulai.com/gdulai/simpleorm/exec" log "gitlab.com/gdulai/simpleloglvl" ) -func cleanUp(dbFile string, conn *simpleorm.DBConnection) { - conn.Close() - os.Remove(dbFile) -} - func TestSelectEmpty(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) - // WHEN selectExec, err := exec.CreateSelect[Test](orm, "") // THEN - if err != nil { log.LogError("Failed to create select. %s", err) t.Fail() @@ -53,15 +40,12 @@ func TestSelectEmpty(t *testing.T) { func TestInsert(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestInsertAndSelectSingle setup failed: %s", err) @@ -69,7 +53,6 @@ func TestInsert(t *testing.T) { return } - // WHEN insertErr := insertExec.Execute(conn) if err != nil { log.LogError("TestInsertAndSelectSingle insert failed: %s", err) @@ -100,16 +83,13 @@ func TestInsert(t *testing.T) { func TestInsertMultiple(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} testObjSecond := Test{Int64Field: 12, IntField: 54, StringField: "fdsa"} + // WHEN insertExec, err := exec.NewInsert[Test](orm, testObj, testObjSecond) if err != nil { log.LogError("TestInsertAndSelectSingle setup failed: %s", err) @@ -117,7 +97,6 @@ func TestInsertMultiple(t *testing.T) { return } - // WHEN insertErr := insertExec.Execute(conn) if err != nil { log.LogError("TestInsertAndSelectSingle insert failed: %s", err) @@ -155,15 +134,12 @@ func TestInsertMultiple(t *testing.T) { func TestInsertAndSelectSingle(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestInsertAndSelectSingle setup failed: %s", err) @@ -178,7 +154,6 @@ func TestInsertAndSelectSingle(t *testing.T) { return } - // WHEN insertErr := insertExec.Execute(conn) selectErr := selectExec.Execute(conn) @@ -225,15 +200,12 @@ func TestInsertAndSelectSingle(t *testing.T) { func TestInsertSelectWithCompositePk(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) testObj := TestWithCompositePk{ID: 12, Name: "Test"} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestInsertSelectWithCompositePk setup failed: %s", err) @@ -248,7 +220,6 @@ func TestInsertSelectWithCompositePk(t *testing.T) { return } - // WHEN err = insertExec.Execute(conn) if err != nil { log.LogError("TestInsertSelectWithCompositePk insert failed: %s", err) @@ -282,14 +253,10 @@ func TestInsertSelectWithCompositePk(t *testing.T) { func TestInsertMultipleAndSelectWithParam(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) - + // WHEN testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} testObjSecond := Test{Int64Field: 12, IntField: 54, StringField: "fdsa"} @@ -314,8 +281,6 @@ func TestInsertMultipleAndSelectWithParam(t *testing.T) { return } - // WHEN - err = insertExec.Execute(conn) if err != nil { log.LogError("TestInsertMultipleAndSelectWithParam first insert failed: %s", err) @@ -356,15 +321,12 @@ func TestInsertMultipleAndSelectWithParam(t *testing.T) { func TestInsertAndUpdate(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestInsertAndUpdate setup failed: %s", err) @@ -379,8 +341,6 @@ func TestInsertAndUpdate(t *testing.T) { return } - // WHEN - err = insertExec.Execute(conn) if err != nil { log.LogError("TestInsertAndUpdate insert failed: %s", err) @@ -441,15 +401,12 @@ func TestInsertAndUpdate(t *testing.T) { func TestInsertAndDelete(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestInsertAndDelete setup failed: %s", err) @@ -479,8 +436,6 @@ func TestInsertAndDelete(t *testing.T) { } deleteExec, err := exec.NewDelete(orm, selectExec.Results()) - - // WHEN err = deleteExec.Execute(conn) // THEN @@ -509,14 +464,12 @@ func TestInsertAndDelete(t *testing.T) { func TestBoolInsertAndSelectSingle(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(TestWithBool{}) - - exec.ExecuteDDL(conn, orm) testObj := TestWithBool{BoolField: true} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestBoolInsertAndSelectSingle setup failed: %s", err) @@ -531,7 +484,6 @@ func TestBoolInsertAndSelectSingle(t *testing.T) { return } - // WHEN err = insertExec.Execute(conn) if err != nil { log.LogError("TestBoolInsertAndSelectSingle insert failed: %s", err) @@ -565,15 +517,13 @@ func TestBoolInsertAndSelectSingle(t *testing.T) { func TestTimeInsertAndSelectSingle(t *testing.T) { // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + orm, conn := testSetup() defer cleanUp("test.db", conn) - orm := simpleorm.NewORM(TestWithTime{}) - - exec.ExecuteDDL(conn, orm) now := time.Now().UnixMilli() testObj := TestWithTime{TimeField: now} + // WHEN insertExec, err := exec.NewInsert(orm, testObj) if err != nil { log.LogError("TestTimeInsertAndSelectSingle setup failed: %s", err) @@ -588,7 +538,6 @@ func TestTimeInsertAndSelectSingle(t *testing.T) { return } - // WHEN err = insertExec.Execute(conn) if err != nil { log.LogError("TestTimeInsertAndSelectSingle insert failed: %s", err) @@ -619,3 +568,116 @@ func TestTimeInsertAndSelectSingle(t *testing.T) { } } + +func TestTransactionExecuteSimple(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + transaction := exec.NewTransaction(conn) + + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + + // WHEN + insertExec, err := exec.NewInsert[Test](orm, testObj) + if err != nil { + log.LogError("TestTransactionExecuteSimple failed: %s", err) + t.Fail() + return + } + + transErr := transaction.Execute(&insertExec) + if transErr != nil { + transErr = transaction.Finish() + } + + // THEN + if transErr != nil { + log.LogError("TestTransactionExecuteSimple failed: %s", err) + t.Fail() + return + } + + insertRes := insertExec.Results() + + if len(insertRes) != 1 { + log.LogError("TestInsertAndSelectSingle test failed. Expected: 1, Actual: %s", len(insertRes)) + t.Fail() + return + } + + singleRes := insertRes[0] + if singleRes.ID != 1 || singleRes.Int64Field != 54 || singleRes.IntField != 12 || singleRes.StringField != "asdf" { + log.LogError("TestInsertAndSelectSingle invalid result. Expected: %s, Actual: %s", testObj, singleRes) + t.Fail() + } +} + +func TestTransactionRollback(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + transaction := exec.NewTransaction(conn) + + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + + // WHEN + insertExec, err := exec.NewInsert[Test](orm, testObj) + if err != nil { + log.LogError("TestTransactionRollback WHEN failed: %s", err) + t.Fail() + return + } + + transErr := transaction.Execute(&insertExec) + if transErr != nil { + log.LogError("TestTransactionRollback WHEN failed: %s", err) + t.Fail() + return + } + + selectExec, err := exec.CreateSelect[Test](orm, "id = ?", insertExec.Results()[0].ID) + if err != nil { + log.LogError("TestTransactionRollback WHEN failed: %s", err) + t.Fail() + return + } + + transErr = transaction.Execute(&selectExec) + if transErr != nil { + log.LogError("TestTransactionRollback WHEN failed: %s", err) + t.Fail() + return + } + + beforeRollbackSelectRes := selectExec.Results() + rollbackErr := transaction.Rollback() + afterRollbackErr := selectExec.Execute(conn) + afterRollbackSelectRes := selectExec.Results() + + // THEN + if len(beforeRollbackSelectRes) != 1 { + log.LogError("TestTransactionExecuteSimple-THEN: Expected 1, Actual: %s", len(beforeRollbackSelectRes)) + t.Fail() + return + } + + if rollbackErr != nil { + log.LogError("TestTransactionExecuteSimple-THEN: rollback error: %s", rollbackErr) + t.Fail() + return + } + + if afterRollbackErr != nil { + log.LogError("TestTransactionExecuteSimple-THEN: after rollback select error: %s", rollbackErr) + t.Fail() + return + } + + if len(afterRollbackSelectRes) != 0 { + log.LogError("TestTransactionExecuteSimple-THEN: Expected 0 after rollback select result") + t.Fail() + return + } +} diff --git a/test/main_test.go b/test/main_test.go index eb0680c..6d36dba 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -4,6 +4,8 @@ import ( "os" "testing" + simpleorm "git.gdulai.com/gdulai/simpleorm" + "git.gdulai.com/gdulai/simpleorm/exec" _ "github.com/mattn/go-sqlite3" log "gitlab.com/gdulai/simpleloglvl" ) @@ -61,3 +63,18 @@ func TestMain(m *testing.M) { os.Exit(code) } + +func testSetup() (*simpleorm.ORM, *simpleorm.DBConnection) { + conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + + orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, + TestWithCompositePk{}, TestWithCompositeFk{}, TestWithBool{}, TestWithTime{}) + exec.ExecuteDDL(conn, orm) + + return orm, conn +} + +func cleanUp(dbFile string, conn *simpleorm.DBConnection) { + conn.Close() + os.Remove(dbFile) +} diff --git a/test/repository_test.go b/test/repository_test.go index 341506d..c80b386 100644 --- a/test/repository_test.go +++ b/test/repository_test.go @@ -3,26 +3,14 @@ package simpleorm_test import ( "testing" - simpleorm "git.gdulai.com/gdulai/simpleorm" "git.gdulai.com/gdulai/simpleorm/exec" "git.gdulai.com/gdulai/simpleorm/repository" log "gitlab.com/gdulai/simpleloglvl" ) -func repoTestSetup() (*simpleorm.ORM, *simpleorm.DBConnection) { - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") - - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - exec.ExecuteDDL(conn, orm) - - return orm, conn -} - func TestRepoSelectAll(t *testing.T) { // GIVEN - orm, conn := repoTestSetup() + orm, conn := testSetup() defer cleanUp("test.db", conn) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} @@ -48,7 +36,7 @@ func TestRepoSelectAll(t *testing.T) { func TestRepoSelectByPk(t *testing.T) { // GIVEN - orm, conn := repoTestSetup() + orm, conn := testSetup() defer cleanUp("test.db", conn) testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} @@ -74,7 +62,7 @@ func TestRepoSelectByPk(t *testing.T) { func TestRepoSelectByCompundPk(t *testing.T) { // GIVEN - orm, conn := repoTestSetup() + orm, conn := testSetup() defer cleanUp("test.db", conn) testObj := TestWithCompositePk{ID: 42069, Name: "Test"} @@ -100,7 +88,7 @@ func TestRepoSelectByCompundPk(t *testing.T) { func TestRepoSave(t *testing.T) { // GIVEN - orm, conn := repoTestSetup() + orm, conn := testSetup() defer cleanUp("test.db", conn) testObj := &Test{Int64Field: 54, IntField: 12, StringField: "asdf"} @@ -121,7 +109,7 @@ func TestRepoSave(t *testing.T) { func TestRepoDelete(t *testing.T) { // GIVEN - orm, conn := repoTestSetup() + orm, conn := testSetup() defer cleanUp("test.db", conn) testObj := &Test{Int64Field: 54, IntField: 12, StringField: "asdf"}