diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 90a771b..e83f587 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -36,6 +36,7 @@ - [eorm, internal/valuer: 摒弃中间表达,直接依赖于 Scan](https://github.com/gotomicro/eorm/pull/79) - [eorm:修改 ErrNoRows 的语义,只有在 Get 才会返回](https://github.com/gotomicro/eorm/pull/80) - [all: Field 取代 FieldByName](https://github.com/gotomicro/eorm/pull/90) +- [eorm: Delete的执行完成](https://github.com/gotomicro/eorm/pull/90) ### 文档, 代码质量以及文档 - [Add examples and docs for Aggregate and Assign](https://github.com/gotomicro/eorm/pull/50) diff --git a/db.go b/db.go index e843b9a..a45b9f1 100644 --- a/db.go +++ b/db.go @@ -82,16 +82,6 @@ func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { return &Tx{tx: tx, db: db}, nil } -// Delete 开始构建一个 DELETE 查询 -func (db *DB) Delete() *Deleter { - return &Deleter{ - builder: builder{ - core: db.core, - buffer: bytebufferpool.Get(), - }, - } -} - // Update 开始构建一个 UPDATE 查询 func (db *DB) Update(table interface{}) *Updater { return &Updater{ diff --git a/db_test.go b/db_test.go index 67f418f..ab16039 100644 --- a/db_test.go +++ b/db_test.go @@ -94,10 +94,10 @@ func ExampleOpen() { // case1 dialect: SQLite } -func ExampleDB_Delete() { +func ExampleNewDeleter() { db := memoryDB() tm := &TestModel{} - query, _ := db.Delete().From(tm).Build() + query, _ := NewDeleter[TestModel](db).From(tm).Build() fmt.Printf("SQL: %s", query.SQL) // Output: // SQL: DELETE FROM `test_model`; diff --git a/delete.go b/delete.go index 4200a23..c5ac431 100644 --- a/delete.go +++ b/delete.go @@ -14,17 +14,39 @@ package eorm +import ( + "context" + "database/sql" + + "github.com/valyala/bytebufferpool" +) + // Deleter builds DELETE query -type Deleter struct { +type Deleter[T any] struct { builder + session table interface{} where []Predicate } +// NewDeleter 开始构建一个 DELETE 查询 +func NewDeleter[T any](sess session) *Deleter[T] { + return &Deleter[T]{ + builder: builder{ + core: sess.getCore(), + buffer: bytebufferpool.Get(), + }, + session: sess, + } +} + // Build returns DELETE query -func (d *Deleter) Build() (*Query, error) { +func (d *Deleter[T]) Build() (*Query, error) { _, _ = d.buffer.WriteString("DELETE FROM ") var err error + if d.table == nil { + d.table = new(T) + } d.meta, err = d.metaRegistry.Get(d.table) if err != nil { return nil, err @@ -43,13 +65,21 @@ func (d *Deleter) Build() (*Query, error) { } // From accepts model definition -func (d *Deleter) From(table interface{}) *Deleter { +func (d *Deleter[T]) From(table interface{}) *Deleter[T] { d.table = table return d } // Where accepts predicates -func (d *Deleter) Where(predicates ...Predicate) *Deleter { +func (d *Deleter[T]) Where(predicates ...Predicate) *Deleter[T] { d.where = predicates return d } + +func (d *Deleter[T]) Exec(ctx context.Context) (sql.Result, error) { + query, err := d.Build() + if err != nil { + return nil, err + } + return newQuerier[T](d.session, query).Exec(ctx) +} diff --git a/delete_test.go b/delete_test.go index 5199d51..f1de177 100644 --- a/delete_test.go +++ b/delete_test.go @@ -15,9 +15,15 @@ package eorm import ( + "context" + "database/sql" + "errors" "fmt" "testing" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) @@ -25,12 +31,12 @@ func TestDeleter_Build(t *testing.T) { testCases := []CommonTestCase{ { name: "no where", - builder: memoryDB().Delete().From(&TestModel{}), + builder: NewDeleter[TestModel](memoryDB()).From(&TestModel{}), wantSql: "DELETE FROM `test_model`;", }, { name: "where", - builder: memoryDB().Delete().From(&TestModel{}).Where(C("Id").EQ(16)), + builder: NewDeleter[TestModel](memoryDB()).Where(C("Id").EQ(16)), wantSql: "DELETE FROM `test_model` WHERE `id`=?;", wantArgs: []interface{}{16}, }, @@ -47,22 +53,102 @@ func TestDeleter_Build(t *testing.T) { } } +func TestDeleter_Exec(t *testing.T) { + + testCases := []struct { + name string + mockOrder func(mock sqlmock.Sqlmock) + delete func(*DB, *testing.T) (sql.Result, error) + wantErr error + wantVal sql.Result + }{ + { + name: "直接删除", + mockOrder: func(mock sqlmock.Sqlmock) { + mock.ExpectExec("DELETE FROM `test_model` WHERE `id`=").WithArgs(1).WillReturnResult(sqlmock.NewResult(100, 1000)) + }, + delete: func(db *DB, t *testing.T) (sql.Result, error) { + deleter := NewDeleter[TestModel](db) + result, err := deleter.From(&TestModel{}).Where(C("Id").EQ(1)).Exec(context.Background()) + return result, err + }, + wantErr: nil, + wantVal: sqlmock.NewResult(100, 1000), + }, + { + name: "事务删除", + mockOrder: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectExec("DELETE FROM `test_model` WHERE `id`=").WithArgs(1).WillReturnResult(sqlmock.NewResult(10, 20)) + mock.ExpectCommit().WillReturnError(errors.New("commit 错误")) + }, + delete: func(db *DB, t *testing.T) (sql.Result, error) { + tx, err := db.BeginTx(context.Background(), &sql.TxOptions{}) + require.NoError(t, err) + + deleter := NewDeleter[TestModel](db) + result, err := deleter.From(&TestModel{}).Where(C("Id").EQ(1)).Exec(context.Background()) + require.NoError(t, err) + + err = tx.Commit() + return result, err + }, + wantErr: errors.New("commit 错误"), + wantVal: sqlmock.NewResult(10, 20), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatal(err) + } + db, err := openDB("mysql", mockDB) + defer func(db *DB) { _ = db.Close() }(db) + if err != nil { + t.Fatal(err) + } + tc.mockOrder(mock) + result, err := tc.delete(db, t) + + assert.Equal(t, tc.wantErr, err) + + rowsAffectedExpect, err := tc.wantVal.RowsAffected() + require.NoError(t, err) + rowsAffected, err := result.RowsAffected() + require.NoError(t, err) + assert.Equal(t, rowsAffectedExpect, rowsAffected) + + lastInsertIdExpected, err := tc.wantVal.LastInsertId() + require.NoError(t, err) + lastInsertId, err := result.LastInsertId() + require.NoError(t, err) + assert.Equal(t, lastInsertIdExpected, lastInsertId) + + if err = mock.ExpectationsWereMet(); err != nil { + t.Error(err) + } + }) + } +} + func ExampleDeleter_Build() { - query, _ := memoryDB().Delete().From(&TestModel{}).Build() + query, _ := NewDeleter[TestModel](memoryDB()).From(&TestModel{}).Build() fmt.Printf("SQL: %s", query.SQL) // Output: // SQL: DELETE FROM `test_model`; } func ExampleDeleter_From() { - query, _ := memoryDB().Delete().From(&TestModel{}).Build() + query, _ := NewDeleter[TestModel](memoryDB()).From(&TestModel{}).Build() fmt.Printf("SQL: %s", query.SQL) // Output: // SQL: DELETE FROM `test_model`; } func ExampleDeleter_Where() { - query, _ := memoryDB().Delete().From(&TestModel{}).Where(C("Id").EQ(12)).Build() + query, _ := NewDeleter[TestModel](memoryDB()).Where(C("Id").EQ(12)).Build() fmt.Printf("SQL: %s\nArgs: %v", query.SQL, query.Args) // Output: // SQL: DELETE FROM `test_model` WHERE `id`=?; diff --git a/internal/integration/delete_test.go b/internal/integration/delete_test.go new file mode 100644 index 0000000..c60db12 --- /dev/null +++ b/internal/integration/delete_test.go @@ -0,0 +1,89 @@ +// Copyright 2021 gotomicro +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package integration + +import ( + "context" + "testing" + + _ "github.com/go-sql-driver/mysql" + "github.com/gotomicro/eorm" + "github.com/gotomicro/eorm/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type DeleteTestSuite struct { + Suite +} + +func (s *DeleteTestSuite) SetupSuite() { + s.Suite.SetupSuite() + data1 := test.NewSimpleStruct(1) + data2 := test.NewSimpleStruct(2) + data3 := test.NewSimpleStruct(3) + _, err := eorm.NewInserter[test.SimpleStruct](s.orm).Values(data1, data2, data3).Exec(context.Background()) + if err != nil { + s.T().Fatal(err) + } +} + +func (i *DeleteTestSuite) TearDownTest() { + _, err := eorm.RawQuery[any](i.orm, "DELETE FROM `simple_struct`").Exec(context.Background()) + if err != nil { + i.T().Fatal(err) + } +} + +func (i *DeleteTestSuite) TestDeleter() { + testCases := []struct { + name string + i *eorm.Deleter[test.SimpleStruct] + rowsAffected int64 + wantErr error + }{ + { + name: "id only", + i: eorm.NewDeleter[test.SimpleStruct](i.orm).From(&test.SimpleStruct{}).Where(eorm.C("Id").EQ("1")), + rowsAffected: 1, + }, + { + name: "all field", + i: eorm.NewDeleter[test.SimpleStruct](i.orm).From(&test.SimpleStruct{}), + rowsAffected: 2, + }, + } + for _, tc := range testCases { + i.T().Run(tc.name, func(t *testing.T) { + res, err := tc.i.Exec(context.Background()) + require.Equal(t, tc.wantErr, err) + affected, err := res.RowsAffected() + require.NotNil(t, err) + assert.Equal(t, tc.rowsAffected, affected) + }) + } +} + +func TestMySQL8tDelete(t *testing.T) { + suite.Run(t, &DeleteTestSuite{ + Suite{ + driver: "mysql", + dsn: "root:root@tcp(localhost:13306)/integration_test", + }, + }) +}