diff --git a/orm/orm.go b/orm/orm_go18.go similarity index 98% rename from orm/orm.go rename to orm/orm_go18.go index fcf82590..e59589aa 100644 --- a/orm/orm.go +++ b/orm/orm_go18.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build go1.8 + // Package orm provide ORM for MySQL/PostgreSQL/sqlite // Simple Usage // @@ -52,6 +54,7 @@ package orm import ( + "context" "database/sql" "errors" "fmt" @@ -458,11 +461,15 @@ func (o *orm) Using(name string) error { // begin transaction func (o *orm) Begin() error { + return o.BeginTx(context.Background(), nil) +} + +func (o *orm) BeginTx(ctx context.Context, opts *sql.TxOptions) error { if o.isTx { return ErrTxHasBegan } var tx *sql.Tx - tx, err := o.db.(txer).Begin() + tx, err := o.db.(txer).BeginTx(ctx, opts) if err != nil { return err } diff --git a/orm/orm_log.go b/orm/orm_log.go index 26c73f9e..979dbbc6 100644 --- a/orm/orm_log.go +++ b/orm/orm_log.go @@ -15,6 +15,7 @@ package orm import ( + "context" "database/sql" "fmt" "io" @@ -150,6 +151,13 @@ func (d *dbQueryLog) Begin() (*sql.Tx, error) { return tx, err } +func (d *dbQueryLog) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { + a := time.Now() + tx, err := d.db.(txer).BeginTx(ctx, opts) + debugLogQueies(d.alias, "db.BeginTx", "START TRANSACTION", a, err) + return tx, err +} + func (d *dbQueryLog) Commit() error { a := time.Now() err := d.db.(txEnder).Commit() diff --git a/orm/orm_test.go b/orm/orm_test.go index ce6fc6d4..ceb814ff 100644 --- a/orm/orm_test.go +++ b/orm/orm_test.go @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build go1.8 + package orm import ( "bytes" + "context" "database/sql" "fmt" "io/ioutil" @@ -452,9 +455,9 @@ func TestNullDataTypes(t *testing.T) { throwFail(t, AssertIs(*d.Float32Ptr, float32Ptr)) throwFail(t, AssertIs(*d.Float64Ptr, float64Ptr)) throwFail(t, AssertIs(*d.DecimalPtr, decimalPtr)) - throwFail(t, AssertIs((*d.TimePtr).Format(testTime), timePtr.Format(testTime))) - throwFail(t, AssertIs((*d.DatePtr).Format(testDate), datePtr.Format(testDate))) - throwFail(t, AssertIs((*d.DateTimePtr).Format(testDateTime), dateTimePtr.Format(testDateTime))) + throwFail(t, AssertIs((*d.TimePtr).UTC().Format(testTime), timePtr.UTC().Format(testTime))) + throwFail(t, AssertIs((*d.DatePtr).UTC().Format(testDate), datePtr.UTC().Format(testDate))) + throwFail(t, AssertIs((*d.DateTimePtr).UTC().Format(testDateTime), dateTimePtr.UTC().Format(testDateTime))) } func TestDataCustomTypes(t *testing.T) { @@ -1990,6 +1993,66 @@ func TestTransaction(t *testing.T) { } +func TestTransactionIsolationLevel(t *testing.T) { + // this test worked when database support transaction isolation level + if IsSqlite { + return + } + + o1 := NewOrm() + o2 := NewOrm() + + // start two transaction with isolation level repeatable read + err := o1.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) + throwFail(t, err) + err = o2.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) + throwFail(t, err) + + // o1 insert tag + var tag Tag + tag.Name = "test-transaction" + id, err := o1.Insert(&tag) + throwFail(t, err) + throwFail(t, AssertIs(id > 0, true)) + + // o2 query tag table, no result + num, err := o2.QueryTable("tag").Filter("name", "test-transaction").Count() + throwFail(t, err) + throwFail(t, AssertIs(num, 0)) + + // o1 commit + o1.Commit() + + // o2 query tag table, still no result + num, err = o2.QueryTable("tag").Filter("name", "test-transaction").Count() + throwFail(t, err) + throwFail(t, AssertIs(num, 0)) + + // o2 commit and query tag table, get the result + o2.Commit() + num, err = o2.QueryTable("tag").Filter("name", "test-transaction").Count() + throwFail(t, err) + throwFail(t, AssertIs(num, 1)) + + num, err = o1.QueryTable("tag").Filter("name", "test-transaction").Delete() + throwFail(t, err) + throwFail(t, AssertIs(num, 1)) +} + +func TestBeginTxWithContextCanceled(t *testing.T) { + o := NewOrm() + ctx, cancel := context.WithCancel(context.Background()) + o.BeginTx(ctx, nil) + id, err := o.Insert(&Tag{Name: "test-context"}) + throwFail(t, err) + throwFail(t, AssertIs(id > 0, true)) + + // cancel the context before commit to make it error + cancel() + err = o.Commit() + throwFail(t, AssertIs(err, context.Canceled)) +} + func TestReadOrCreate(t *testing.T) { u := &User{ UserName: "Kyle", @@ -2260,6 +2323,7 @@ func TestIgnoreCaseTag(t *testing.T) { throwFail(t, AssertIs(info.fields.GetByName("Name02").column, "Name")) throwFail(t, AssertIs(info.fields.GetByName("Name03").column, "name")) } + func TestInsertOrUpdate(t *testing.T) { RegisterModel(new(User)) user := User{UserName: "unique_username133", Status: 1, Password: "o"} diff --git a/orm/types.go b/orm/types.go index e3373096..2fdc98c7 100644 --- a/orm/types.go +++ b/orm/types.go @@ -15,6 +15,7 @@ package orm import ( + "context" "database/sql" "reflect" "time" @@ -106,6 +107,17 @@ type Ormer interface { // ... // err = o.Rollback() Begin() error + // begin transaction with provided context and option + // the provided context is used until the transaction is committed or rolled back. + // if the context is canceled, the transaction will be rolled back. + // the provided TxOptions is optional and may be nil if defaults should be used. + // if a non-default isolation level is used that the driver doesn't support, an error will be returned. + // for example: + // o := NewOrm() + // err := o.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) + // ... + // err = o.Rollback() + BeginTx(ctx context.Context, opts *sql.TxOptions) error // commit transaction Commit() error // rollback transaction @@ -401,6 +413,7 @@ type dbQuerier interface { // transaction beginner type txer interface { Begin() (*sql.Tx, error) + BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) } // transaction ending