diff --git a/contracts/database/db/db.go b/contracts/database/db/db.go index aae36d907..ca4f35803 100644 --- a/contracts/database/db/db.go +++ b/contracts/database/db/db.go @@ -1,18 +1,81 @@ package db -import "database/sql" +import ( + "context" + "database/sql" +) type DB interface { Table(name string) Query + WithContext(ctx context.Context) DB } type Query interface { + // Avg(column string) (any, error) + // Count(dest *int64) error + // Chunk(size int, callback func(rows []any) error) error + // CrossJoin(table string, on any, args ...any) Query + // DoesntExist() (bool, error) + // Distinct() Query + // dump + // dumpRawSql + // Each(callback func(rows []any) error) error + // Exists() (bool, error) + // Find(dest any, conds ...any) error First(dest any) error + // firstOrFail + // decrement Delete() (*Result, error) Get(dest any) error + // GroupBy(column string) Query + // GroupByRaw(query string, args ...any) Query + // having + // HavingRaw(query any, args ...any) Query + // increment + // inRandomOrder Insert(data any) (*Result, error) + // incrementEach + // insertGetId + // Join(table string, on any, args ...any) Query + // latest + // LeftJoin(table string, on any, args ...any) Query + // limit + // lockForUpdate + // Max(column string) (any, error) + // offset + // OrderBy(column string) Query + // orderByDesc + // OrderByRaw(query string, args ...any) Query + // OrWhere(query any, args ...any) Query + // OrWhereLike() + // OrWhereNotLike + // Pluck(column string, dest any) error + // RightJoin(table string, on any, args ...any) Query + // Select(dest any, columns ...string) error + // SelectRaw(query string, args ...any) (any, error) + // sharedLock + // skip + // take Update(data any) (*Result, error) + // updateOrInsert + // Value(column string, dest any) error + // when Where(query any, args ...any) Query + // WhereAll() + // WhereAny() + // whereBetween + // whereColumn + // whereExists + // WhereLike() + // WhereIn() + // WhereNone() + // WhereNot() + // whereNotBetween + // whereNotIn + // WhereNotLike() + // whereNotNull + // WhereNull(column string) Query + // WhereRaw(query string, args ...any) Query } type Result struct { diff --git a/contracts/database/driver/driver.go b/contracts/database/driver/driver.go index 36409d2df..725bcc3dd 100644 --- a/contracts/database/driver/driver.go +++ b/contracts/database/driver/driver.go @@ -15,6 +15,8 @@ type Driver interface { Config() database.Config DB() (*sql.DB, error) Docker() (docker.DatabaseDriver, error) + // Explain generate SQL string with given parameters + Explain(sql string, vars ...any) string Gorm() (*gorm.DB, GormQuery, error) Grammar() schema.Grammar Processor() schema.Processor diff --git a/contracts/database/logger/logger.go b/contracts/database/logger/logger.go new file mode 100644 index 000000000..51481f9ef --- /dev/null +++ b/contracts/database/logger/logger.go @@ -0,0 +1,28 @@ +package logger + +import ( + "context" + + "gorm.io/gorm/logger" + + "github.com/goravel/framework/support/carbon" +) + +// Level log level +type Level int + +const ( + Silent Level = iota + 1 + Error + Warn + Info +) + +type Logger interface { + Level(Level) Logger + Info(context.Context, string, ...any) + Warn(context.Context, string, ...any) + Error(context.Context, string, ...any) + Trace(ctx context.Context, begin carbon.Carbon, sql string, rowsAffected int64, err error) + ToGorm() logger.Interface +} diff --git a/database/db/db.go b/database/db/db.go index 31c5aeb85..0e948665b 100644 --- a/database/db/db.go +++ b/database/db/db.go @@ -1,27 +1,32 @@ package db import ( + "context" "fmt" "github.com/jmoiron/sqlx" "github.com/goravel/framework/contracts/config" - "github.com/goravel/framework/contracts/database" "github.com/goravel/framework/contracts/database/db" contractsdriver "github.com/goravel/framework/contracts/database/driver" + contractslogger "github.com/goravel/framework/contracts/database/logger" + "github.com/goravel/framework/contracts/log" + "github.com/goravel/framework/database/logger" "github.com/goravel/framework/errors" ) type DB struct { builder db.Builder - config database.Config + ctx context.Context + driver contractsdriver.Driver + logger contractslogger.Logger } -func NewDB(config database.Config, builder db.Builder) db.DB { - return &DB{config: config, builder: builder} +func NewDB(ctx context.Context, driver contractsdriver.Driver, logger contractslogger.Logger, builder db.Builder) db.DB { + return &DB{ctx: ctx, driver: driver, logger: logger, builder: builder} } -func BuildDB(config config.Config, connection string) (db.DB, error) { +func BuildDB(config config.Config, log log.Log, connection string) (db.DB, error) { driverCallback, exist := config.Get(fmt.Sprintf("database.connections.%s.via", connection)).(func() (contractsdriver.Driver, error)) if !exist { return nil, errors.DatabaseConfigNotFound @@ -37,9 +42,13 @@ func BuildDB(config config.Config, connection string) (db.DB, error) { return nil, err } - return NewDB(driver.Config(), sqlx.NewDb(instance, driver.Config().Driver)), nil + return NewDB(context.Background(), driver, logger.NewLogger(config, log), sqlx.NewDb(instance, driver.Config().Driver)), nil } func (r *DB) Table(name string) db.Query { - return NewQuery(r.config, r.builder, name) + return NewQuery(r.ctx, r.driver, r.builder, r.logger, name) +} + +func (r *DB) WithContext(ctx context.Context) db.DB { + return NewDB(ctx, r.driver, r.logger, r.builder) } diff --git a/database/db/db_test.go b/database/db/db_test.go index 24541d6d7..f83491e93 100644 --- a/database/db/db_test.go +++ b/database/db/db_test.go @@ -32,9 +32,11 @@ func TestBuildDB(t *testing.T) { driverCallback := func() (contractsdriver.Driver, error) { return mockDriver, nil } - mockConfig.On("Get", "database.connections.mysql.via").Return(driverCallback) - mockDriver.On("DB").Return(&sql.DB{}, nil) - mockDriver.On("Config").Return(database.Config{Driver: "mysql"}) + mockConfig.EXPECT().Get("database.connections.mysql.via").Return(driverCallback).Once() + mockDriver.EXPECT().DB().Return(&sql.DB{}, nil).Once() + mockDriver.EXPECT().Config().Return(database.Config{Driver: "mysql"}).Once() + mockConfig.EXPECT().GetBool("app.debug").Return(false).Once() + mockConfig.EXPECT().GetInt("database.slow_threshold", 200).Return(200).Once() }, expectedError: nil, }, @@ -42,7 +44,7 @@ func TestBuildDB(t *testing.T) { name: "Config Not Found", connection: "invalid", setup: func() { - mockConfig.On("Get", "database.connections.invalid.via").Return(nil) + mockConfig.EXPECT().Get("database.connections.invalid.via").Return(nil).Once() }, expectedError: errors.DatabaseConfigNotFound, }, @@ -54,7 +56,7 @@ func TestBuildDB(t *testing.T) { mockDriver = mocksdriver.NewDriver(t) test.setup() - db, err := BuildDB(mockConfig, test.connection) + db, err := BuildDB(mockConfig, nil, test.connection) if test.expectedError != nil { assert.Equal(t, test.expectedError, err) assert.Nil(t, db) diff --git a/database/db/query.go b/database/db/query.go index 779fa3a05..63dbb6284 100644 --- a/database/db/query.go +++ b/database/db/query.go @@ -1,51 +1,62 @@ package db import ( - "fmt" + "context" + databasesql "database/sql" + "reflect" "sort" sq "github.com/Masterminds/squirrel" "github.com/goravel/framework/contracts/database" "github.com/goravel/framework/contracts/database/db" + "github.com/goravel/framework/contracts/database/driver" + "github.com/goravel/framework/contracts/database/logger" "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/carbon" "github.com/goravel/framework/support/str" ) type Query struct { builder db.Builder conditions Conditions - config database.Config + ctx context.Context + driver driver.Driver + logger logger.Logger } -func NewQuery(config database.Config, builder db.Builder, table string) *Query { +func NewQuery(ctx context.Context, driver driver.Driver, builder db.Builder, logger logger.Logger, table string) *Query { return &Query{ builder: builder, conditions: Conditions{ table: table, }, - config: config, + driver: driver, + ctx: ctx, + logger: logger, } } func (r *Query) Delete() (*db.Result, error) { sql, args, err := r.buildDelete() - // TODO: use logger instead of println - fmt.Println(sql, args, err) if err != nil { return nil, err } result, err := r.builder.Exec(sql, args...) if err != nil { + r.trace(sql, args, -1, err) return nil, err } rowsAffected, err := result.RowsAffected() if err != nil { + r.trace(sql, args, -1, err) return nil, err } + r.trace(sql, args, rowsAffected, nil) + return &db.Result{ RowsAffected: rowsAffected, }, nil @@ -53,24 +64,52 @@ func (r *Query) Delete() (*db.Result, error) { func (r *Query) First(dest any) error { sql, args, err := r.buildSelect() - // TODO: use logger instead of println - fmt.Println(sql, args, err) if err != nil { return err } - return r.builder.Get(dest, sql, args...) + err = r.builder.Get(dest, sql, args...) + if err != nil { + if errors.Is(err, databasesql.ErrNoRows) { + r.trace(sql, args, 0, nil) + return nil + } + + r.trace(sql, args, -1, err) + + return err + } + + r.trace(sql, args, 1, nil) + + return nil } func (r *Query) Get(dest any) error { sql, args, err := r.buildSelect() - // TODO: use logger instead of println - fmt.Println(sql, args, err) if err != nil { return err } - return r.builder.Select(dest, sql, args...) + err = r.builder.Select(dest, sql, args...) + if err != nil { + r.trace(sql, args, -1, err) + return err + } + + destValue := reflect.ValueOf(dest) + if destValue.Kind() == reflect.Ptr { + destValue = destValue.Elem() + } + + rowsAffected := int64(-1) + if destValue.Kind() == reflect.Slice { + rowsAffected = int64(destValue.Len()) + } + + r.trace(sql, args, rowsAffected, nil) + + return nil } func (r *Query) Insert(data any) (*db.Result, error) { @@ -88,18 +127,21 @@ func (r *Query) Insert(data any) (*db.Result, error) { if err != nil { return nil, err } - // TODO: use logger instead of println - fmt.Println(sql, args, err) + result, err := r.builder.Exec(sql, args...) if err != nil { + r.trace(sql, args, -1, err) return nil, err } rowsAffected, err := result.RowsAffected() if err != nil { + r.trace(sql, args, -1, err) return nil, err } + r.trace(sql, args, rowsAffected, nil) + return &db.Result{ RowsAffected: rowsAffected, }, nil @@ -112,29 +154,31 @@ func (r *Query) Update(data any) (*db.Result, error) { } sql, args, err := r.buildUpdate(mapData) - // TODO: use logger instead of println - fmt.Println(sql, args, err) if err != nil { return nil, err } result, err := r.builder.Exec(sql, args...) if err != nil { + r.trace(sql, args, -1, err) return nil, err } rowsAffected, err := result.RowsAffected() if err != nil { + r.trace(sql, args, -1, err) return nil, err } + r.trace(sql, args, rowsAffected, nil) + return &db.Result{ RowsAffected: rowsAffected, }, nil } func (r *Query) Where(query any, args ...any) db.Query { - q := NewQuery(r.config, r.builder, r.conditions.table) + q := NewQuery(r.ctx, r.driver, r.builder, r.logger, r.conditions.table) q.conditions = r.conditions q.conditions.where = append(r.conditions.where, Where{ query: query, @@ -150,8 +194,8 @@ func (r *Query) buildDelete() (sql string, args []any, err error) { } builder := sq.Delete(r.conditions.table) - if r.config.PlaceholderFormat != nil { - builder = builder.PlaceholderFormat(r.config.PlaceholderFormat) + if placeholderFormat := r.placeholderFormat(); placeholderFormat != nil { + builder = builder.PlaceholderFormat(placeholderFormat) } for _, where := range r.conditions.where { @@ -179,8 +223,8 @@ func (r *Query) buildInsert(data []map[string]any) (sql string, args []any, err } builder := sq.Insert(r.conditions.table) - if r.config.PlaceholderFormat != nil { - builder = builder.PlaceholderFormat(r.config.PlaceholderFormat) + if placeholderFormat := r.placeholderFormat(); placeholderFormat != nil { + builder = builder.PlaceholderFormat(placeholderFormat) } first := data[0] @@ -208,8 +252,8 @@ func (r *Query) buildSelect() (sql string, args []any, err error) { } builder := sq.Select("*") - if r.config.PlaceholderFormat != nil { - builder = builder.PlaceholderFormat(r.config.PlaceholderFormat) + if placeholderFormat := r.placeholderFormat(); placeholderFormat != nil { + builder = builder.PlaceholderFormat(placeholderFormat) } builder = builder.From(r.conditions.table) @@ -239,8 +283,8 @@ func (r *Query) buildUpdate(data map[string]any) (sql string, args []any, err er } builder := sq.Update(r.conditions.table) - if r.config.PlaceholderFormat != nil { - builder = builder.PlaceholderFormat(r.config.PlaceholderFormat) + if placeholderFormat := r.placeholderFormat(); placeholderFormat != nil { + builder = builder.PlaceholderFormat(placeholderFormat) } for _, where := range r.conditions.where { @@ -263,3 +307,15 @@ func (r *Query) buildUpdate(data map[string]any) (sql string, args []any, err er return builder.ToSql() } + +func (r *Query) placeholderFormat() database.PlaceholderFormat { + if r.driver.Config().PlaceholderFormat != nil { + return r.driver.Config().PlaceholderFormat + } + + return nil +} + +func (r *Query) trace(sql string, args []any, rowsAffected int64, err error) { + r.logger.Trace(r.ctx, carbon.Now(), r.driver.Explain(sql, args...), rowsAffected, err) +} diff --git a/database/db/query_test.go b/database/db/query_test.go index 7697f707a..35da87414 100644 --- a/database/db/query_test.go +++ b/database/db/query_test.go @@ -1,6 +1,8 @@ package db import ( + "context" + databasesql "database/sql" "testing" "github.com/stretchr/testify/assert" @@ -10,6 +12,9 @@ import ( "github.com/goravel/framework/contracts/database" "github.com/goravel/framework/errors" mocksdb "github.com/goravel/framework/mocks/database/db" + mocksdriver "github.com/goravel/framework/mocks/database/driver" + mockslogger "github.com/goravel/framework/mocks/database/logger" + "github.com/goravel/framework/support/carbon" ) // TestUser is a test model @@ -22,7 +27,11 @@ type TestUser struct { type QueryTestSuite struct { suite.Suite + ctx context.Context mockBuilder *mocksdb.Builder + mockDriver *mocksdriver.Driver + mockLogger *mockslogger.Logger + now carbon.Carbon query *Query } @@ -31,37 +40,126 @@ func TestQueryTestSuite(t *testing.T) { } func (s *QueryTestSuite) SetupTest() { + s.ctx = context.Background() s.mockBuilder = mocksdb.NewBuilder(s.T()) - s.query = NewQuery(database.Config{}, s.mockBuilder, "users") + s.mockDriver = mocksdriver.NewDriver(s.T()) + s.mockLogger = mockslogger.NewLogger(s.T()) + s.now = carbon.Now() + carbon.SetTestNow(s.now) + + s.query = NewQuery(s.ctx, s.mockDriver, s.mockBuilder, s.mockLogger, "users") } func (s *QueryTestSuite) TestDelete() { - mockResult := &MockResult{} - mockResult.On("RowsAffected").Return(int64(1), nil) - s.mockBuilder.EXPECT().Exec("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return(mockResult, nil).Once() + s.Run("success", func() { + mockResult := &MockResult{} + mockResult.On("RowsAffected").Return(int64(1), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Exec("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return("DELETE FROM users WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "DELETE FROM users WHERE name = \"John\" AND id = 1", int64(1), nil).Return().Once() + + result, err := s.query.Where("name", "John").Where("id", 1).Delete() + s.Nil(err) + s.Equal(int64(1), result.RowsAffected) + + mockResult.AssertExpectations(s.T()) + }) - result, err := s.query.Where("name", "John").Where("id", 1).Delete() - s.Nil(err) - s.Equal(int64(1), result.RowsAffected) + s.Run("failed to exec", func() { + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Exec("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return(nil, assert.AnError).Once() + s.mockDriver.EXPECT().Explain("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return("DELETE FROM users WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "DELETE FROM users WHERE name = \"John\" AND id = 1", int64(-1), assert.AnError).Return().Once() - mockResult.AssertExpectations(s.T()) + _, err := s.query.Where("name", "John").Where("id", 1).Delete() + s.Equal(assert.AnError, err) + }) + + s.Run("failed to get rows affected", func() { + mockResult := &MockResult{} + mockResult.On("RowsAffected").Return(int64(0), assert.AnError).Once() + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Exec("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("DELETE FROM users WHERE name = ? AND id = ?", "John", 1).Return("DELETE FROM users WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "DELETE FROM users WHERE name = \"John\" AND id = 1", int64(-1), assert.AnError).Return().Once() + + _, err := s.query.Where("name", "John").Where("id", 1).Delete() + s.Equal(assert.AnError, err) + }) } func (s *QueryTestSuite) TestFirst() { - var user TestUser - s.mockBuilder.EXPECT().Get(&user, "SELECT * FROM users WHERE name = ?", "John").Return(nil).Once() + s.Run("success", func() { + var user TestUser - err := s.query.Where("name", "John").First(&user) - s.Nil(err) + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Get(&user, "SELECT * FROM users WHERE name = ?", "John").Return(nil).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE name = ?", "John").Return("SELECT * FROM users WHERE name = \"John\"").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE name = \"John\"", int64(1), nil).Return().Once() + + err := s.query.Where("name", "John").First(&user) + + s.Nil(err) + }) + + s.Run("failed to get", func() { + var user TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Get(&user, "SELECT * FROM users WHERE name = ?", "John").Return(assert.AnError).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE name = ?", "John").Return("SELECT * FROM users WHERE name = \"John\"").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE name = \"John\"", int64(-1), assert.AnError).Return().Once() + + err := s.query.Where("name", "John").First(&user) + + s.Equal(assert.AnError, err) + }) + + s.Run("no rows", func() { + var user TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Get(&user, "SELECT * FROM users WHERE name = ?", "John").Return(databasesql.ErrNoRows).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE name = ?", "John").Return("SELECT * FROM users WHERE name = \"John\"").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE name = \"John\"", int64(0), nil).Return().Once() + + err := s.query.Where("name", "John").First(&user) + + s.Nil(err) + }) } func (s *QueryTestSuite) TestGet() { - var users []TestUser - s.mockBuilder.EXPECT().Select(&users, "SELECT * FROM users WHERE age = ?", 25).Return(nil).Once() + s.Run("success", func() { + var users []TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Select(&users, "SELECT * FROM users WHERE age = ?", 25).Run(func(dest any, query string, args ...any) { + destUsers := dest.(*[]TestUser) + *destUsers = []TestUser{{ID: 1, Name: "John", Age: 25}, {ID: 2, Name: "Jane", Age: 30}} + }).Return(nil).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE age = ?", 25).Return("SELECT * FROM users WHERE age = 25").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE age = 25", int64(2), nil).Return().Once() + + err := s.query.Where("age", 25).Get(&users) + s.Nil(err) + s.mockBuilder.AssertExpectations(s.T()) + }) + + s.Run("failed to get", func() { + var users []TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Select(&users, "SELECT * FROM users WHERE age = ?", 25).Return(assert.AnError).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE age = ?", 25).Return("SELECT * FROM users WHERE age = 25").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE age = 25", int64(-1), assert.AnError).Return().Once() - err := s.query.Where("age", 25).Get(&users) - s.Nil(err) - s.mockBuilder.AssertExpectations(s.T()) + err := s.query.Where("age", 25).Get(&users) + s.Equal(assert.AnError, err) + }) } func (s *QueryTestSuite) TestInsert() { @@ -80,7 +178,11 @@ func (s *QueryTestSuite) TestInsert() { mockResult := &MockResult{} mockResult.On("RowsAffected").Return(int64(1), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("INSERT INTO users (id) VALUES (?)", uint(1)).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("INSERT INTO users (id) VALUES (?)", uint(1)).Return("INSERT INTO users (id) VALUES (1)").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "INSERT INTO users (id) VALUES (1)", int64(1), nil).Return().Once() result, err := s.query.Insert(user) s.Nil(err) @@ -97,7 +199,11 @@ func (s *QueryTestSuite) TestInsert() { mockResult := &MockResult{} mockResult.On("RowsAffected").Return(int64(2), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("INSERT INTO users (id) VALUES (?),(?)", uint(1), uint(2)).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("INSERT INTO users (id) VALUES (?),(?)", uint(1), uint(2)).Return("INSERT INTO users (id) VALUES (1),(2)").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "INSERT INTO users (id) VALUES (1),(2)", int64(2), nil).Return().Once() result, err := s.query.Insert(users) s.Nil(err) @@ -115,7 +221,11 @@ func (s *QueryTestSuite) TestInsert() { mockResult := &MockResult{} mockResult.On("RowsAffected").Return(int64(1), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("INSERT INTO users (age,id,name) VALUES (?,?,?)", 25, 1, "John").Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("INSERT INTO users (age,id,name) VALUES (?,?,?)", 25, 1, "John").Return("INSERT INTO users (age,id,name) VALUES (25,1,\"John\")").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "INSERT INTO users (age,id,name) VALUES (25,1,\"John\")", int64(1), nil).Return().Once() result, err := s.query.Insert(user) s.Nil(err) @@ -132,7 +242,11 @@ func (s *QueryTestSuite) TestInsert() { mockResult := &MockResult{} mockResult.On("RowsAffected").Return(int64(2), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("INSERT INTO users (age,id,name) VALUES (?,?,?),(?,?,?)", 25, 1, "John", 30, 2, "Jane").Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("INSERT INTO users (age,id,name) VALUES (?,?,?),(?,?,?)", 25, 1, "John", 30, 2, "Jane").Return("INSERT INTO users (age,id,name) VALUES (25,1,\"John\"),(30,2,\"Jane\")").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "INSERT INTO users (age,id,name) VALUES (25,1,\"John\"),(30,2,\"Jane\")", int64(2), nil).Return().Once() result, err := s.query.Insert(users) s.Nil(err) @@ -155,7 +269,10 @@ func (s *QueryTestSuite) TestInsert() { Age: 25, } + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("INSERT INTO users (id) VALUES (?)", uint(1)).Return(nil, assert.AnError).Once() + s.mockDriver.EXPECT().Explain("INSERT INTO users (id) VALUES (?)", uint(1)).Return("INSERT INTO users (id) VALUES (1)").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "INSERT INTO users (id) VALUES (1)", int64(-1), assert.AnError).Return().Once() result, err := s.query.Insert(user) s.Nil(result) @@ -173,7 +290,11 @@ func (s *QueryTestSuite) TestUpdate() { mockResult := &MockResult{} mockResult.On("RowsAffected").Return(int64(1), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("UPDATE users SET phone = ? WHERE name = ? AND id = ?", "1234567890", "John", 1).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("UPDATE users SET phone = ? WHERE name = ? AND id = ?", "1234567890", "John", 1).Return("UPDATE users SET phone = \"1234567890\" WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "UPDATE users SET phone = \"1234567890\" WHERE name = \"John\" AND id = 1", int64(1), nil).Return().Once() result, err := s.query.Where("name", "John").Where("id", 1).Update(user) s.Nil(err) @@ -191,7 +312,11 @@ func (s *QueryTestSuite) TestUpdate() { mockResult := &MockResult{} mockResult.On("RowsAffected").Return(int64(1), nil) + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Exec("UPDATE users SET age = ?, name = ?, phone = ? WHERE name = ? AND id = ?", 25, "John", "1234567890", "John", 1).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("UPDATE users SET age = ?, name = ?, phone = ? WHERE name = ? AND id = ?", 25, "John", "1234567890", "John", 1).Return("UPDATE users SET age = 25, name = \"John\", phone = \"1234567890\" WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "UPDATE users SET age = 25, name = \"John\", phone = \"1234567890\" WHERE name = \"John\" AND id = 1", int64(1), nil).Return().Once() result, err := s.query.Where("name", "John").Where("id", 1).Update(user) s.Nil(err) @@ -199,12 +324,56 @@ func (s *QueryTestSuite) TestUpdate() { mockResult.AssertExpectations(s.T()) }) + + s.Run("failed to exec", func() { + user := TestUser{ + Phone: "1234567890", + Name: "John", + Age: 25, + } + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Exec("UPDATE users SET phone = ? WHERE name = ? AND id = ?", "1234567890", "John", 1).Return(nil, assert.AnError).Once() + s.mockDriver.EXPECT().Explain("UPDATE users SET phone = ? WHERE name = ? AND id = ?", "1234567890", "John", 1).Return("UPDATE users SET phone = \"1234567890\" WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "UPDATE users SET phone = \"1234567890\" WHERE name = \"John\" AND id = 1", int64(-1), assert.AnError).Return().Once() + + result, err := s.query.Where("name", "John").Where("id", 1).Update(user) + s.Nil(result) + s.Equal(assert.AnError, err) + }) + + s.Run("failed to get rows affected", func() { + user := TestUser{ + Phone: "1234567890", + Name: "John", + Age: 25, + } + + mockResult := &MockResult{} + mockResult.On("RowsAffected").Return(int64(0), assert.AnError).Once() + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() + s.mockBuilder.EXPECT().Exec("UPDATE users SET phone = ? WHERE name = ? AND id = ?", "1234567890", "John", 1).Return(mockResult, nil).Once() + s.mockDriver.EXPECT().Explain("UPDATE users SET phone = ? WHERE name = ? AND id = ?", "1234567890", "John", 1).Return("UPDATE users SET phone = \"1234567890\" WHERE name = \"John\" AND id = 1").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "UPDATE users SET phone = \"1234567890\" WHERE name = \"John\" AND id = 1", int64(-1), assert.AnError).Return().Once() + + result, err := s.query.Where("name", "John").Where("id", 1).Update(user) + s.Nil(result) + s.Equal(assert.AnError, err) + }) } func (s *QueryTestSuite) TestWhere() { + now := carbon.Now() + carbon.SetTestNow(now) + s.Run("simple where condition", func() { var user TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Get(&user, "SELECT * FROM users WHERE name = ?", "John").Return(nil).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE name = ?", "John").Return("SELECT * FROM users WHERE name = \"John\"").Once() + s.mockLogger.EXPECT().Trace(s.ctx, now, "SELECT * FROM users WHERE name = \"John\"", int64(1), nil).Return().Once() err := s.query.Where("name", "John").First(&user) s.Nil(err) @@ -212,7 +381,11 @@ func (s *QueryTestSuite) TestWhere() { s.Run("where with multiple arguments", func() { var users []TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Select(&users, "SELECT * FROM users WHERE age IN (?,?)", 25, 30).Return(nil).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE age IN (?,?)", 25, 30).Return("SELECT * FROM users WHERE age IN (25,30)").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE age IN (25,30)", int64(0), nil).Return().Once() err := s.query.Where("age", []int{25, 30}).Get(&users) s.Nil(err) @@ -220,7 +393,11 @@ func (s *QueryTestSuite) TestWhere() { s.Run("where with raw query", func() { var users []TestUser + + s.mockDriver.EXPECT().Config().Return(database.Config{}).Once() s.mockBuilder.EXPECT().Select(&users, "SELECT * FROM users WHERE age > ?", 18).Return(nil).Once() + s.mockDriver.EXPECT().Explain("SELECT * FROM users WHERE age > ?", 18).Return("SELECT * FROM users WHERE age > 18").Once() + s.mockLogger.EXPECT().Trace(s.ctx, s.now, "SELECT * FROM users WHERE age > 18", int64(0), nil).Return().Once() err := s.query.Where("age > ?", 18).Get(&users) s.Nil(err) diff --git a/database/gorm/logger.go b/database/logger/logger.go similarity index 62% rename from database/gorm/logger.go rename to database/logger/logger.go index 3258d3985..f977c9e70 100644 --- a/database/gorm/logger.go +++ b/database/logger/logger.go @@ -1,4 +1,4 @@ -package gorm +package logger import ( "context" @@ -9,14 +9,15 @@ import ( "strings" "time" - "gorm.io/gorm/logger" + gormlogger "gorm.io/gorm/logger" "github.com/goravel/framework/contracts/config" + "github.com/goravel/framework/contracts/database/logger" "github.com/goravel/framework/contracts/log" - "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/carbon" ) -func NewLogger(config config.Config, log log.Log) logger.Interface { +func NewLogger(config config.Config, log log.Log) logger.Logger { level := logger.Warn if config.GetBool("app.debug") { level = logger.Info @@ -36,32 +37,28 @@ func NewLogger(config config.Config, log log.Log) logger.Interface { type Logger struct { log log.Log - level logger.LogLevel + level logger.Level slowThreshold time.Duration } -// LogMode log mode -func (r *Logger) LogMode(level logger.LogLevel) logger.Interface { +func (r *Logger) Level(level logger.Level) logger.Logger { r.level = level return r } -// Info print info func (r *Logger) Info(ctx context.Context, msg string, data ...any) { if r.level >= logger.Info { r.log.Infof(msg, data...) } } -// Warn print warn messages func (r *Logger) Warn(ctx context.Context, msg string, data ...any) { if r.level >= logger.Warn { r.log.Warningf(msg, data...) } } -// Error print error messages func (r *Logger) Error(ctx context.Context, msg string, data ...any) { // Let upper layer function deals with connection refused error var cancel bool @@ -89,8 +86,7 @@ func (r *Logger) Error(ctx context.Context, msg string, data ...any) { } } -// Trace print sql message -func (r *Logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { +func (r *Logger) Trace(ctx context.Context, begin carbon.Carbon, sql string, rowsAffected int64, err error) { if r.level <= logger.Silent { return } @@ -101,32 +97,67 @@ func (r *Logger) Trace(ctx context.Context, begin time.Time, fc func() (string, traceErrStr = "[%.3fms] [rows:%v] %s\t%s" ) - elapsed := time.Since(begin) + elapsed := begin.DiffInDuration() + switch { - case err != nil && r.level >= logger.Error && !errors.Is(err, logger.ErrRecordNotFound): - sql, rows := fc() - if rows == -1 { + case err != nil && r.level >= logger.Error: + if rowsAffected == -1 { r.log.Errorf(traceErrStr, float64(elapsed.Nanoseconds())/1e6, "-", sql, err) } else { - r.log.Errorf(traceErrStr, float64(elapsed.Nanoseconds())/1e6, rows, sql, err) + r.log.Errorf(traceErrStr, float64(elapsed.Nanoseconds())/1e6, rowsAffected, sql, err) } case elapsed > r.slowThreshold && r.slowThreshold != 0 && r.level >= logger.Warn: - sql, rows := fc() - if rows == -1 { + if rowsAffected == -1 { r.log.Warningf(traceWarnStr, float64(elapsed.Nanoseconds())/1e6, "-", sql) } else { - r.log.Warningf(traceWarnStr, float64(elapsed.Nanoseconds())/1e6, rows, sql) + r.log.Warningf(traceWarnStr, float64(elapsed.Nanoseconds())/1e6, rowsAffected, sql) } case r.level == logger.Info: - sql, rows := fc() - if rows == -1 { + if rowsAffected == -1 { r.log.Infof(traceStr, float64(elapsed.Nanoseconds())/1e6, "-", sql) } else { - r.log.Infof(traceStr, float64(elapsed.Nanoseconds())/1e6, rows, sql) + r.log.Infof(traceStr, float64(elapsed.Nanoseconds())/1e6, rowsAffected, sql) } } } +func (r *Logger) ToGorm() gormlogger.Interface { + return NewGorm(r) +} + +type Gorm struct { + logger logger.Logger +} + +func NewGorm(logger logger.Logger) *Gorm { + return &Gorm{ + logger: logger, + } +} + +func (r *Gorm) LogMode(level gormlogger.LogLevel) gormlogger.Interface { + _ = r.logger.Level(GormLevelToLevel(level)) + + return r +} + +func (r *Gorm) Info(ctx context.Context, msg string, data ...any) { + r.logger.Info(ctx, msg, data...) +} + +func (r *Gorm) Warn(ctx context.Context, msg string, data ...any) { + r.logger.Warn(ctx, msg, data...) +} + +func (r *Gorm) Error(ctx context.Context, msg string, data ...any) { + r.logger.Error(ctx, msg, data...) +} + +func (r *Gorm) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { + sql, rowsAffected := fc() + r.logger.Trace(ctx, carbon.FromStdTime(begin), sql, rowsAffected, err) +} + // FileWithLineNum return the file name and line number of the current file func FileWithLineNum() string { _, file, _, _ := runtime.Caller(0) @@ -143,3 +174,16 @@ func FileWithLineNum() string { return "" } + +func GormLevelToLevel(level gormlogger.LogLevel) logger.Level { + switch level { + case gormlogger.Silent: + return logger.Silent + case gormlogger.Error: + return logger.Error + case gormlogger.Warn: + return logger.Warn + default: + return logger.Info + } +} diff --git a/database/gorm/logger_test.go b/database/logger/logger_test.go similarity index 93% rename from database/gorm/logger_test.go rename to database/logger/logger_test.go index ff12b2b5e..d152f06f7 100644 --- a/database/gorm/logger_test.go +++ b/database/logger/logger_test.go @@ -1,4 +1,4 @@ -package gorm +package logger import ( "context" @@ -10,10 +10,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" - "gorm.io/gorm/logger" + "github.com/goravel/framework/contracts/database/logger" mocksconfig "github.com/goravel/framework/mocks/config" mockslog "github.com/goravel/framework/mocks/log" + "github.com/goravel/framework/support/carbon" ) func TestNewLogger(t *testing.T) { @@ -23,7 +24,7 @@ func TestNewLogger(t *testing.T) { tests := []struct { name string setup func() - wantLevel logger.LogLevel + wantLevel logger.Level wantSlow time.Duration }{ { @@ -86,8 +87,8 @@ func (s *LoggerTestSuite) SetupTest() { } } -func (s *LoggerTestSuite) TestLogMode() { - result := s.logger.LogMode(logger.Error) +func (s *LoggerTestSuite) TestLevel() { + result := s.logger.Level(logger.Error) s.Equal(logger.Error, s.logger.level) s.Equal(s.logger, result) } @@ -146,7 +147,7 @@ func (s *LoggerTestSuite) TestTrace() { rows int64 elapsed time.Duration err error - level logger.LogLevel + level logger.Level setup func() }{ { @@ -218,10 +219,8 @@ func (s *LoggerTestSuite) TestTrace() { tt.setup() s.logger.level = tt.level - begin := time.Now().Add(-tt.elapsed) - s.logger.Trace(context.Background(), begin, func() (string, int64) { - return sql, tt.rows - }, tt.err) + begin := carbon.Now().SubDuration(tt.elapsed.String()) + s.logger.Trace(context.Background(), begin, sql, tt.rows, tt.err) }) } } diff --git a/database/service_provider.go b/database/service_provider.go index 7bd66adaa..5509c1769 100644 --- a/database/service_provider.go +++ b/database/service_provider.go @@ -56,12 +56,17 @@ func (r *ServiceProvider) Register(app foundation.Application) { return nil, errors.ConfigFacadeNotSet.SetModule(errors.ModuleDB) } + log := app.MakeLog() + if log == nil { + return nil, errors.LogFacadeNotSet.SetModule(errors.ModuleDB) + } + connection := config.GetString("database.default") if connection == "" { return nil, nil } - return db.BuildDB(config, connection) + return db.BuildDB(config, log, connection) }) app.Singleton(contracts.BindingSchema, func(app foundation.Application) (any, error) { diff --git a/mocks/database/db/DB.go b/mocks/database/db/DB.go index 608d0f9ef..faaf9228e 100644 --- a/mocks/database/db/DB.go +++ b/mocks/database/db/DB.go @@ -3,6 +3,8 @@ package db import ( + context "context" + db "github.com/goravel/framework/contracts/database/db" mock "github.com/stretchr/testify/mock" ) @@ -68,6 +70,54 @@ func (_c *DB_Table_Call) RunAndReturn(run func(string) db.Query) *DB_Table_Call return _c } +// WithContext provides a mock function with given fields: ctx +func (_m *DB) WithContext(ctx context.Context) db.DB { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for WithContext") + } + + var r0 db.DB + if rf, ok := ret.Get(0).(func(context.Context) db.DB); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(db.DB) + } + } + + return r0 +} + +// DB_WithContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithContext' +type DB_WithContext_Call struct { + *mock.Call +} + +// WithContext is a helper method to define mock.On call +// - ctx context.Context +func (_e *DB_Expecter) WithContext(ctx interface{}) *DB_WithContext_Call { + return &DB_WithContext_Call{Call: _e.mock.On("WithContext", ctx)} +} + +func (_c *DB_WithContext_Call) Run(run func(ctx context.Context)) *DB_WithContext_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *DB_WithContext_Call) Return(_a0 db.DB) *DB_WithContext_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_WithContext_Call) RunAndReturn(run func(context.Context) db.DB) *DB_WithContext_Call { + _c.Call.Return(run) + return _c +} + // NewDB creates a new instance of DB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewDB(t interface { diff --git a/mocks/database/driver/Driver.go b/mocks/database/driver/Driver.go index 8b84a1809..55beebfe1 100644 --- a/mocks/database/driver/Driver.go +++ b/mocks/database/driver/Driver.go @@ -189,6 +189,63 @@ func (_c *Driver_Docker_Call) RunAndReturn(run func() (docker.DatabaseDriver, er return _c } +// Explain provides a mock function with given fields: _a0, vars +func (_m *Driver) Explain(_a0 string, vars ...interface{}) string { + var _ca []interface{} + _ca = append(_ca, _a0) + _ca = append(_ca, vars...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Explain") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string, ...interface{}) string); ok { + r0 = rf(_a0, vars...) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Driver_Explain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Explain' +type Driver_Explain_Call struct { + *mock.Call +} + +// Explain is a helper method to define mock.On call +// - _a0 string +// - vars ...interface{} +func (_e *Driver_Expecter) Explain(_a0 interface{}, vars ...interface{}) *Driver_Explain_Call { + return &Driver_Explain_Call{Call: _e.mock.On("Explain", + append([]interface{}{_a0}, vars...)...)} +} + +func (_c *Driver_Explain_Call) Run(run func(_a0 string, vars ...interface{})) *Driver_Explain_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Driver_Explain_Call) Return(_a0 string) *Driver_Explain_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Driver_Explain_Call) RunAndReturn(run func(string, ...interface{}) string) *Driver_Explain_Call { + _c.Call.Return(run) + return _c +} + // Gorm provides a mock function with no fields func (_m *Driver) Gorm() (*gorm.DB, driver.GormQuery, error) { ret := _m.Called() diff --git a/mocks/database/logger/Logger.go b/mocks/database/logger/Logger.go new file mode 100644 index 000000000..cbc260aa2 --- /dev/null +++ b/mocks/database/logger/Logger.go @@ -0,0 +1,309 @@ +// Code generated by mockery. DO NOT EDIT. + +package logger + +import ( + context "context" + + carbon "github.com/dromara/carbon/v2" + + gormlogger "gorm.io/gorm/logger" + + logger "github.com/goravel/framework/contracts/database/logger" + + mock "github.com/stretchr/testify/mock" +) + +// Logger is an autogenerated mock type for the Logger type +type Logger struct { + mock.Mock +} + +type Logger_Expecter struct { + mock *mock.Mock +} + +func (_m *Logger) EXPECT() *Logger_Expecter { + return &Logger_Expecter{mock: &_m.Mock} +} + +// Error provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Logger) Error(_a0 context.Context, _a1 string, _a2 ...interface{}) { + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _a2...) + _m.Called(_ca...) +} + +// Logger_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' +type Logger_Error_Call struct { + *mock.Call +} + +// Error is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +// - _a2 ...interface{} +func (_e *Logger_Expecter) Error(_a0 interface{}, _a1 interface{}, _a2 ...interface{}) *Logger_Error_Call { + return &Logger_Error_Call{Call: _e.mock.On("Error", + append([]interface{}{_a0, _a1}, _a2...)...)} +} + +func (_c *Logger_Error_Call) Run(run func(_a0 context.Context, _a1 string, _a2 ...interface{})) *Logger_Error_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Logger_Error_Call) Return() *Logger_Error_Call { + _c.Call.Return() + return _c +} + +func (_c *Logger_Error_Call) RunAndReturn(run func(context.Context, string, ...interface{})) *Logger_Error_Call { + _c.Run(run) + return _c +} + +// Info provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Logger) Info(_a0 context.Context, _a1 string, _a2 ...interface{}) { + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _a2...) + _m.Called(_ca...) +} + +// Logger_Info_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Info' +type Logger_Info_Call struct { + *mock.Call +} + +// Info is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +// - _a2 ...interface{} +func (_e *Logger_Expecter) Info(_a0 interface{}, _a1 interface{}, _a2 ...interface{}) *Logger_Info_Call { + return &Logger_Info_Call{Call: _e.mock.On("Info", + append([]interface{}{_a0, _a1}, _a2...)...)} +} + +func (_c *Logger_Info_Call) Run(run func(_a0 context.Context, _a1 string, _a2 ...interface{})) *Logger_Info_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Logger_Info_Call) Return() *Logger_Info_Call { + _c.Call.Return() + return _c +} + +func (_c *Logger_Info_Call) RunAndReturn(run func(context.Context, string, ...interface{})) *Logger_Info_Call { + _c.Run(run) + return _c +} + +// Level provides a mock function with given fields: _a0 +func (_m *Logger) Level(_a0 logger.Level) logger.Logger { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Level") + } + + var r0 logger.Logger + if rf, ok := ret.Get(0).(func(logger.Level) logger.Logger); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.Logger) + } + } + + return r0 +} + +// Logger_Level_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Level' +type Logger_Level_Call struct { + *mock.Call +} + +// Level is a helper method to define mock.On call +// - _a0 logger.Level +func (_e *Logger_Expecter) Level(_a0 interface{}) *Logger_Level_Call { + return &Logger_Level_Call{Call: _e.mock.On("Level", _a0)} +} + +func (_c *Logger_Level_Call) Run(run func(_a0 logger.Level)) *Logger_Level_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(logger.Level)) + }) + return _c +} + +func (_c *Logger_Level_Call) Return(_a0 logger.Logger) *Logger_Level_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Logger_Level_Call) RunAndReturn(run func(logger.Level) logger.Logger) *Logger_Level_Call { + _c.Call.Return(run) + return _c +} + +// ToGorm provides a mock function with no fields +func (_m *Logger) ToGorm() gormlogger.Interface { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ToGorm") + } + + var r0 gormlogger.Interface + if rf, ok := ret.Get(0).(func() gormlogger.Interface); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(gormlogger.Interface) + } + } + + return r0 +} + +// Logger_ToGorm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToGorm' +type Logger_ToGorm_Call struct { + *mock.Call +} + +// ToGorm is a helper method to define mock.On call +func (_e *Logger_Expecter) ToGorm() *Logger_ToGorm_Call { + return &Logger_ToGorm_Call{Call: _e.mock.On("ToGorm")} +} + +func (_c *Logger_ToGorm_Call) Run(run func()) *Logger_ToGorm_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Logger_ToGorm_Call) Return(_a0 gormlogger.Interface) *Logger_ToGorm_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Logger_ToGorm_Call) RunAndReturn(run func() gormlogger.Interface) *Logger_ToGorm_Call { + _c.Call.Return(run) + return _c +} + +// Trace provides a mock function with given fields: ctx, begin, sql, rowsAffected, err +func (_m *Logger) Trace(ctx context.Context, begin carbon.Carbon, sql string, rowsAffected int64, err error) { + _m.Called(ctx, begin, sql, rowsAffected, err) +} + +// Logger_Trace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trace' +type Logger_Trace_Call struct { + *mock.Call +} + +// Trace is a helper method to define mock.On call +// - ctx context.Context +// - begin carbon.Carbon +// - sql string +// - rowsAffected int64 +// - err error +func (_e *Logger_Expecter) Trace(ctx interface{}, begin interface{}, sql interface{}, rowsAffected interface{}, err interface{}) *Logger_Trace_Call { + return &Logger_Trace_Call{Call: _e.mock.On("Trace", ctx, begin, sql, rowsAffected, err)} +} + +func (_c *Logger_Trace_Call) Run(run func(ctx context.Context, begin carbon.Carbon, sql string, rowsAffected int64, err error)) *Logger_Trace_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(carbon.Carbon), args[2].(string), args[3].(int64), args[4].(error)) + }) + return _c +} + +func (_c *Logger_Trace_Call) Return() *Logger_Trace_Call { + _c.Call.Return() + return _c +} + +func (_c *Logger_Trace_Call) RunAndReturn(run func(context.Context, carbon.Carbon, string, int64, error)) *Logger_Trace_Call { + _c.Run(run) + return _c +} + +// Warn provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Logger) Warn(_a0 context.Context, _a1 string, _a2 ...interface{}) { + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _a2...) + _m.Called(_ca...) +} + +// Logger_Warn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warn' +type Logger_Warn_Call struct { + *mock.Call +} + +// Warn is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +// - _a2 ...interface{} +func (_e *Logger_Expecter) Warn(_a0 interface{}, _a1 interface{}, _a2 ...interface{}) *Logger_Warn_Call { + return &Logger_Warn_Call{Call: _e.mock.On("Warn", + append([]interface{}{_a0, _a1}, _a2...)...)} +} + +func (_c *Logger_Warn_Call) Run(run func(_a0 context.Context, _a1 string, _a2 ...interface{})) *Logger_Warn_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Logger_Warn_Call) Return() *Logger_Warn_Call { + _c.Call.Return() + return _c +} + +func (_c *Logger_Warn_Call) RunAndReturn(run func(context.Context, string, ...interface{})) *Logger_Warn_Call { + _c.Run(run) + return _c +} + +// NewLogger creates a new instance of Logger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLogger(t interface { + mock.TestingT + Cleanup(func()) +}) *Logger { + mock := &Logger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/support/carbon/carbon.go b/support/carbon/carbon.go index 56f3428b6..deec4657e 100644 --- a/support/carbon/carbon.go +++ b/support/carbon/carbon.go @@ -7,26 +7,27 @@ import ( ) type Clock struct { + testNow bool + testTime Carbon timezone string } -var testCarbon = carbon.NewCarbon() - var clock = &Clock{} // SetTestNow Set the test time. Remember to unset after used. func SetTestNow(testTime Carbon) { - testCarbon.SetTestNow(testTime) + clock.testNow = true + clock.testTime = testTime } // UnsetTestNow Unset the test time. func UnsetTestNow() { - testCarbon.UnSetTestNow() + clock.testNow = false } // IsTestNow Determine if the test now time is set. func IsTestNow() bool { - return testCarbon.IsSetTestNow() + return clock.testNow } // SetTimezone sets timezone. @@ -39,7 +40,7 @@ func SetTimezone(timezone string) { // Now return a Carbon object of now. func Now(timezone ...string) Carbon { if IsTestNow() { - return testCarbon.Now(getTimezone(timezone)) + return clock.testTime } return carbon.Now(getTimezone(timezone)) diff --git a/tests/query.go b/tests/query.go index 0b129935b..0f037732d 100644 --- a/tests/query.go +++ b/tests/query.go @@ -13,6 +13,7 @@ import ( contractsdocker "github.com/goravel/framework/contracts/testing/docker" databasedb "github.com/goravel/framework/database/db" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/logger" mocksconfig "github.com/goravel/framework/mocks/config" "github.com/goravel/framework/support/docker" "github.com/goravel/framework/support/str" @@ -48,7 +49,7 @@ func NewTestQuery(ctx context.Context, driver contractsdriver.Driver, config con testQuery := &TestQuery{ config: config, - db: databasedb.NewDB(driver.Config(), sqlx.NewDb(db, driver.Config().Driver)), + db: databasedb.NewDB(ctx, driver, logger.NewLogger(config, utils.NewTestLog()), sqlx.NewDb(db, driver.Config().Driver)), driver: driver, query: gorm.NewQuery(ctx, config, driver.Config(), query, gormQuery, utils.NewTestLog(), nil, nil), }