Permalink
Browse files

emulate nested transaction using savepoint (#37)

* emulate nested transaction using savepoint

* tests for nested transaction

* fix error message

* fix postgres adapter

* fix stackoverflow
  • Loading branch information...
Fs02 committed Jan 6, 2019
1 parent e837849 commit 670607d4c6d1bcc4aef9d6c34e4064b704d1bba9
Showing with 189 additions and 15 deletions.
  1. +2 −5 adapter/postgres/postgres.go
  2. +83 −0 adapter/specs/transaction.go
  3. +33 −10 adapter/sql/sql.go
  4. +34 −0 adapter/sql/sql_test.go
  5. +15 −0 go.mod
  6. +22 −0 go.sum
@@ -81,13 +81,10 @@ func (adapter *Adapter) InsertAll(query grimoire.Query, fields []string, allchan

// Begin begins a new transaction.
func (adapter *Adapter) Begin() (grimoire.Adapter, error) {
Tx, err := adapter.DB.Begin()
newAdapter, err := adapter.Adapter.Begin()

return &Adapter{
&sql.Adapter{
Config: adapter.Config,
Tx: Tx,
},
Adapter: newAdapter.(*sql.Adapter),
}, err
}

@@ -38,6 +38,10 @@ func Transaction(t *testing.T, repo grimoire.Repo) {
{"InsertWithAssocError", insertWithAssocError, errors.New("let's rollback", "", errors.NotFound)},
{"InsertWithAssocPanic", insertWithAssocPanic, errors.New("let's rollback", "", errors.NotFound)},
{"ReplaceAssoc", replaceAssoc, nil},
{"NestedInsertWithAssoc", nestedInsertWithAssoc, nil},
{"NestedInsertWithAssocError", nestedInsertWithAssocError, errors.New("let's rollback", "", errors.NotFound)},
{"NestedInsertWithAssocPanic", nestedInsertWithAssocPanic, errors.New("let's rollback", "", errors.NotFound)},
{"NestedReplaceAssoc", nestedReplaceAssoc, nil},
}

for _, tt := range tests {
@@ -126,6 +130,85 @@ func replaceAssoc(t *testing.T) func(repo grimoire.Repo) error {
}
}

func nestedInsertWithAssoc(t *testing.T) func(repo grimoire.Repo) error {
user := User{}

ch := changeUser(user, input)
assert.Nil(t, ch.Error())

// transaction block
return func(repo grimoire.Repo) error {
repo.From("users").MustInsert(&user, ch)

return repo.Transaction(func(repo grimoire.Repo) error {
addresses := ch.Changes()["addresses"].([]*changeset.Changeset)
repo.From("addresses").Set("user_id", user.ID).MustInsert(&user.Addresses, addresses...)

return nil
})
}
}

func nestedInsertWithAssocError(t *testing.T) func(repo grimoire.Repo) error {
user := User{}

ch := changeUser(user, input)
assert.Nil(t, ch.Error())

// transaction block
return func(repo grimoire.Repo) error {
repo.From("users").MustInsert(&user, ch)

return repo.Transaction(func(repo grimoire.Repo) error {
addresses := ch.Changes()["addresses"].([]*changeset.Changeset)
repo.From("addresses").Set("user_id", user.ID).MustInsert(&user.Addresses, addresses...)

// should rollback
return errors.New("let's rollback", "", errors.NotFound)
})
}
}

func nestedInsertWithAssocPanic(t *testing.T) func(repo grimoire.Repo) error {
user := User{}

ch := changeUser(user, input)
assert.Nil(t, ch.Error())

// transaction block
return func(repo grimoire.Repo) error {
repo.From("users").MustInsert(&user, ch)

return repo.Transaction(func(repo grimoire.Repo) error {
addresses := ch.Changes()["addresses"].([]*changeset.Changeset)
repo.From("addresses").Set("user_id", user.ID).MustInsert(&user.Addresses, addresses...)

// should rollback
panic(errors.New("let's rollback", "", errors.NotFound))
})
}
}

func nestedReplaceAssoc(t *testing.T) func(repo grimoire.Repo) error {
user := User{}

ch := changeUser(user, input)
assert.Nil(t, ch.Error())

// transaction block
return func(repo grimoire.Repo) error {
repo.From("users").MustOne(&user)
addresses := ch.Changes()["addresses"].([]*changeset.Changeset)

return repo.Transaction(func(repo grimoire.Repo) error {
repo.From("addresses").Where(c.Eq(c.I("user_id"), user.ID)).MustDelete()
repo.From("addresses").Set("user_id", user.ID).MustInsert(&user.Addresses, addresses...)

return nil
})
}
}

func changeUser(user interface{}, params params.Params) *changeset.Changeset {
ch := changeset.Cast(user, params, []string{
"name",
@@ -3,6 +3,7 @@ package sql

import (
"database/sql"
"strconv"
"time"

"github.com/Fs02/grimoire"
@@ -21,9 +22,10 @@ type Config struct {

// Adapter definition for mysql database.
type Adapter struct {
Config *Config
DB *sql.DB
Tx *sql.Tx
Config *Config
DB *sql.DB
Tx *sql.Tx
savepoint int
}

var _ grimoire.Adapter = (*Adapter)(nil)
@@ -92,31 +94,52 @@ func (adapter *Adapter) Delete(query grimoire.Query, loggers ...grimoire.Logger)

// Begin begins a new transaction.
func (adapter *Adapter) Begin() (grimoire.Adapter, error) {
Tx, err := adapter.DB.Begin()
var tx *sql.Tx
var savepoint int
var err error

if adapter.Tx != nil {
tx = adapter.Tx
savepoint = adapter.savepoint + 1
_, _, err = adapter.Exec("SAVEPOINT s"+strconv.Itoa(savepoint)+";", []interface{}{})
} else {
tx, err = adapter.DB.Begin()
}

return &Adapter{
Config: adapter.Config,
Tx: Tx,
Config: adapter.Config,
Tx: tx,
savepoint: savepoint,
}, err
}

// Commit commits current transaction.
func (adapter *Adapter) Commit() error {
var err error

if adapter.Tx == nil {
return errors.NewUnexpected("not in transaction")
err = errors.NewUnexpected("unable to commit outside transaction")
} else if adapter.savepoint > 0 {
_, _, err = adapter.Exec("RELEASE SAVEPOINT s"+strconv.Itoa(adapter.savepoint)+";", []interface{}{})
} else {
err = adapter.Tx.Commit()
}

err := adapter.Tx.Commit()
return adapter.Config.ErrorFunc(err)
}

// Rollback revert current transaction.
func (adapter *Adapter) Rollback() error {
var err error

if adapter.Tx == nil {
return errors.NewUnexpected("not in transaction")
err = errors.NewUnexpected("unable to rollback outside transaction")
} else if adapter.savepoint > 0 {
_, _, err = adapter.Exec("ROLLBACK TO SAVEPOINT s"+strconv.Itoa(adapter.savepoint)+";", []interface{}{})
} else {
err = adapter.Tx.Rollback()
}

err := adapter.Tx.Rollback()
return adapter.Config.ErrorFunc(err)
}

@@ -121,6 +121,40 @@ func TestAdapter_Transaction_rollback(t *testing.T) {
assert.NotNil(t, err)
}

func TestAdapter_Transaction_nestedCommit(t *testing.T) {
adapter, err := open()
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

result := struct {
Name string
}{}
ch := changeset.Convert(result)

err = grimoire.New(adapter).Transaction(func(repo grimoire.Repo) error {
return repo.Transaction(func(repo grimoire.Repo) error {
repo.From("test").MustInsert(&result, ch)
return nil
})
})

assert.Nil(t, err)
}

func TestAdapter_Transaction_nestedRollback(t *testing.T) {
adapter, err := open()
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

err = grimoire.New(adapter).Transaction(func(repo grimoire.Repo) error {
return repo.Transaction(func(repo grimoire.Repo) error {
return errors.New("", "", errors.UniqueConstraint)
})
})

assert.NotNil(t, err)
}

func TestAdapter_InsertAll_error(t *testing.T) {
adapter, err := open()
paranoid.Panic(err, "failed to open database connection")
15 go.mod

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
22 go.sum

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.

0 comments on commit 670607d

Please sign in to comment.