Repository: go-gorm/gorm
Pull request #5499 (2022-07-09 06:48:56)

test(MigrateColumn): mock alter column to improve field compare
<!--
Make sure these boxes checked before submitting your pull request.

For significant changes, please open an issue to make an agreement on an implementation design/plan first before starting it.
-->

- [x] Do only one thing
- [x] Non breaking API changes
- [x] Tested

### What did this pull request do?

<!--
provide a general description of the code changes in your pull request
-->

A way for improve compare field in different drivers.
We can find the following problems
postgres: 
```log
smallint -> int2
integer -> int4 
bigint -> int8
decimal ->  numeric
```
mysql:
```log
bool -> tinyint
```
sqlserver:
```go
value, ok = columnType.DefaultValue()
// ok alway true, means default value not null
```
### User Case Description

<!-- Your use case -->


a631807682: @jinzhu Yes, this PR should not be merged unless the issues that occurred in all drivers are fixed. Now its purpose is to find the problem in driver MigrateColum.

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 16903ed..b88bf67 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -9,3 +9,12 @@ linters:
     - prealloc
     - unconvert
     - unparam
+    - goimports
+    - whitespace
+
+linters-settings:
+  whitespace:
+    multi-func: true
+  goimports:
+    local-prefixes: gorm.io/gorm
+
diff --git a/README.md b/README.md
index 5bb1be3..68fa660 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,12 @@ The fantastic ORM library for Golang, aims to be developer friendly.
 
 [You can help to deliver a better GORM, check out things you can do](https://gorm.io/contribute.html)
 
+## Contributors
+
+Thank you for contributing to the GORM framework!
+
+[![Contributors](https://contrib.rocks/image?repo=go-gorm/gorm)](https://github.com/go-gorm/gorm/graphs/contributors)
+
 ## License
 
 © Jinzhu, 2013~time.Now
diff --git a/callbacks/query.go b/callbacks/query.go
index 6793676..97fe8a4 100644
--- a/callbacks/query.go
+++ b/callbacks/query.go
@@ -185,7 +185,7 @@ func BuildQuerySQL(db *gorm.DB) {
 					}
 
 					fromClause.Joins = append(fromClause.Joins, clause.Join{
-						Type:  clause.LeftJoin,
+						Type:  join.JoinType,
 						Table: clause.Table{Name: relation.FieldSchema.Table, Alias: tableAliasName},
 						ON:    clause.Where{Exprs: exprs},
 					})
diff --git a/chainable_api.go b/chainable_api.go
index 6d48d56..8a92a9e 100644
--- a/chainable_api.go
+++ b/chainable_api.go
@@ -13,7 +13,7 @@ import (
 //
 //	// update all users's name to `hello`
 //	db.Model(&User{}).Update("name", "hello")
-//	// if user's primary key is non-blank, will use it as condition, then will only update the user's name to `hello`
+//	// if user's primary key is non-blank, will use it as condition, then will only update that user's name to `hello`
 //	db.Model(&user).Update("name", "hello")
 func (db *DB) Model(value interface{}) (tx *DB) {
 	tx = db.getInstance()
@@ -22,6 +22,19 @@ func (db *DB) Model(value interface{}) (tx *DB) {
 }
 
 // Clauses Add clauses
+//
+// This supports both standard clauses (clause.OrderBy, clause.Limit, clause.Where) and more
+// advanced techniques like specifying lock strength and optimizer hints. See the
+// [docs] for more depth.
+//
+//	// add a simple limit clause
+//	db.Clauses(clause.Limit{Limit: 1}).Find(&User{})
+//	// tell the optimizer to use the `idx_user_name` index
+//	db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
+//	// specify the lock strength to UPDATE
+//	db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
+//
+// [docs]: https://gorm.io/docs/sql_builder.html#Clauses
 func (db *DB) Clauses(conds ...clause.Expression) (tx *DB) {
 	tx = db.getInstance()
 	var whereConds []interface{}
@@ -45,6 +58,9 @@ func (db *DB) Clauses(conds ...clause.Expression) (tx *DB) {
 var tableRegexp = regexp.MustCompile(`(?i).+? AS (\w+)\s*(?:$|,)`)
 
 // Table specify the table you would like to run db operations
+//
+//	// Get a user
+//	db.Table("users").take(&result)
 func (db *DB) Table(name string, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	if strings.Contains(name, " ") || strings.Contains(name, "`") || len(args) > 0 {
@@ -66,6 +82,11 @@ func (db *DB) Table(name string, args ...interface{}) (tx *DB) {
 }
 
 // Distinct specify distinct fields that you want querying
+//
+//	// Select distinct names of users
+//	db.Distinct("name").Find(&results)
+//	// Select distinct name/age pairs from users
+//	db.Distinct("name", "age").Find(&results)
 func (db *DB) Distinct(args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	tx.Statement.Distinct = true
@@ -76,6 +97,14 @@ func (db *DB) Distinct(args ...interface{}) (tx *DB) {
 }
 
 // Select specify fields that you want when querying, creating, updating
+//
+// Use Select when you only want a subset of the fields. By default, GORM will select all fields.
+// Select accepts both string arguments and arrays.
+//
+//	// Select name and age of user using multiple arguments
+//	db.Select("name", "age").Find(&users)
+//	// Select name and age of user using an array
+//	db.Select([]string{"name", "age"}).Find(&users)
 func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 
@@ -153,6 +182,17 @@ func (db *DB) Omit(columns ...string) (tx *DB) {
 }
 
 // Where add conditions
+//
+// See the [docs] for details on the various formats that where clauses can take. By default, where clauses chain with AND.
+//
+//	// Find the first user with name jinzhu
+//	db.Where("name = ?", "jinzhu").First(&user)
+//	// Find the first user with name jinzhu and age 20
+//	db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
+//	// Find the first user with name jinzhu and age not equal to 20
+//	db.Where("name = ?", "jinzhu").Where("age <> ?", "20").First(&user)
+//
+// [docs]: https://gorm.io/docs/query.html#Conditions
 func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
@@ -162,6 +202,11 @@ func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
 }
 
 // Not add NOT conditions
+//
+// Not works similarly to where, and has the same syntax.
+//
+//	// Find the first user with name not equal to jinzhu
+//	db.Not("name = ?", "jinzhu").First(&user)
 func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
@@ -171,6 +216,11 @@ func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB) {
 }
 
 // Or add OR conditions
+//
+// Or is used to chain together queries with an OR.
+//
+//	// Find the first user with name equal to jinzhu or john
+//	db.Where("name = ?", "jinzhu").Or("name = ?", "john").First(&user)
 func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
@@ -185,6 +235,16 @@ func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB) {
 //	db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Find(&user)
 //	db.Joins("Account", DB.Select("id").Where("user_id = users.id AND name = ?", "someName").Model(&Account{}))
 func (db *DB) Joins(query string, args ...interface{}) (tx *DB) {
+	return joins(db, clause.LeftJoin, query, args...)
+}
+
+// InnerJoins specify inner joins conditions
+// db.InnerJoins("Account").Find(&user)
+func (db *DB) InnerJoins(query string, args ...interface{}) (tx *DB) {
+	return joins(db, clause.InnerJoin, query, args...)
+}
+
+func joins(db *DB, joinType clause.JoinType, query string, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 
 	if len(args) == 1 {
@@ -198,11 +258,14 @@ func (db *DB) Joins(query string, args ...interface{}) (tx *DB) {
 		}
 	}
 
-	tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args})
+	tx.Statement.Joins = append(tx.Statement.Joins, join{Name: query, Conds: args, JoinType: joinType})
 	return
 }
 
 // Group specify the group method on the find
+//
+//	// Select the sum age of users with given names
+//	db.Model(&User{}).Select("name, sum(age) as total").Group("name").Find(&results)
 func (db *DB) Group(name string) (tx *DB) {
 	tx = db.getInstance()
 
@@ -214,6 +277,9 @@ func (db *DB) Group(name string) (tx *DB) {
 }
 
 // Having specify HAVING conditions for GROUP BY
+//
+//	// Select the sum age of users with name jinzhu
+//	db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "jinzhu").Find(&result)
 func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	tx.Statement.AddClause(clause.GroupBy{
@@ -222,7 +288,7 @@ func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB) {
 	return
 }
 
-// Order specify order when retrieve records from database
+// Order specify order when retrieving records from database
 //
 //	db.Order("name DESC")
 //	db.Order(clause.OrderByColumn{Column: clause.Column{Name: "name"}, Desc: true})
@@ -247,6 +313,13 @@ func (db *DB) Order(value interface{}) (tx *DB) {
 }
 
 // Limit specify the number of records to be retrieved
+//
+// Limit conditions can be cancelled by using `Limit(-1)`.
+//
+//	// retrieve 3 users
+//	db.Limit(3).Find(&users)
+//	// retrieve 3 users into users1, and all users into users2
+//	db.Limit(3).Find(&users1).Limit(-1).Find(&users2)
 func (db *DB) Limit(limit int) (tx *DB) {
 	tx = db.getInstance()
 	tx.Statement.AddClause(clause.Limit{Limit: &limit})
@@ -254,6 +327,13 @@ func (db *DB) Limit(limit int) (tx *DB) {
 }
 
 // Offset specify the number of records to skip before starting to return the records
+//
+// Offset conditions can be cancelled by using `Offset(-1)`.
+//
+//	// select the third user
+//	db.Offset(2).First(&user)
+//	// select the first user by cancelling an earlier chained offset
+//	db.Offset(5).Offset(-1).First(&user)
 func (db *DB) Offset(offset int) (tx *DB) {
 	tx = db.getInstance()
 	tx.Statement.AddClause(clause.Limit{Offset: offset})
@@ -281,6 +361,7 @@ func (db *DB) Scopes(funcs ...func(*DB) *DB) (tx *DB) {
 
 // Preload preload associations with given conditions
 //
+//	// get all users, and preload all non-cancelled orders
 //	db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
 func (db *DB) Preload(query string, args ...interface{}) (tx *DB) {
 	tx = db.getInstance()
@@ -291,12 +372,41 @@ func (db *DB) Preload(query string, args ...interface{}) (tx *DB) {
 	return
 }
 
+// Attrs provide attributes used in [FirstOrCreate] or [FirstOrInit]
+//
+// Attrs only adds attributes if the record is not found.
+//
+//	// assign an email if the record is not found
+//	db.Where(User{Name: "non_existing"}).Attrs(User{Email: "fake@fake.org"}).FirstOrInit(&user)
+//	// user -> User{Name: "non_existing", Email: "fake@fake.org"}
+//
+//	// assign an email if the record is not found, otherwise ignore provided email
+//	db.Where(User{Name: "jinzhu"}).Attrs(User{Email: "fake@fake.org"}).FirstOrInit(&user)
+//	// user -> User{Name: "jinzhu", Age: 20}
+//
+// [FirstOrCreate]: https://gorm.io/docs/advanced_query.html#FirstOrCreate
+// [FirstOrInit]: https://gorm.io/docs/advanced_query.html#FirstOrInit
 func (db *DB) Attrs(attrs ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	tx.Statement.attrs = attrs
 	return
 }
 
+// Assign provide attributes used in [FirstOrCreate] or [FirstOrInit]
+//
+// Assign adds attributes even if the record is found. If using FirstOrCreate, this means that
+// records will be updated even if they are found.
+//
+//	// assign an email regardless of if the record is not found
+//	db.Where(User{Name: "non_existing"}).Assign(User{Email: "fake@fake.org"}).FirstOrInit(&user)
+//	// user -> User{Name: "non_existing", Email: "fake@fake.org"}
+//
+//	// assign email regardless of if record is found
+//	db.Where(User{Name: "jinzhu"}).Assign(User{Email: "fake@fake.org"}).FirstOrInit(&user)
+//	// user -> User{Name: "jinzhu", Age: 20, Email: "fake@fake.org"}
+//
+// [FirstOrCreate]: https://gorm.io/docs/advanced_query.html#FirstOrCreate
+// [FirstOrInit]: https://gorm.io/docs/advanced_query.html#FirstOrInit
 func (db *DB) Assign(attrs ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	tx.Statement.assigns = attrs
diff --git a/finisher_api.go b/finisher_api.go
index 5516c0a..39d9fca 100644
--- a/finisher_api.go
+++ b/finisher_api.go
@@ -231,7 +231,11 @@ func (db *DB) FindInBatches(dest interface{}, batchSize int, fc func(tx *DB, bat
 			break
 		}
 
-		primaryValue, _ := result.Statement.Schema.PrioritizedPrimaryField.ValueOf(tx.Statement.Context, resultsValue.Index(resultsValue.Len()-1))
+		primaryValue, zero := result.Statement.Schema.PrioritizedPrimaryField.ValueOf(tx.Statement.Context, resultsValue.Index(resultsValue.Len()-1))
+		if zero {
+			tx.AddError(ErrPrimaryKeyRequired)
+			break
+		}
 		queryDB = tx.Clauses(clause.Gt{Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey}, Value: primaryValue})
 	}
 
@@ -290,6 +294,16 @@ func (db *DB) assignInterfacesToValue(values ...interface{}) {
 
 // FirstOrInit finds the first matching record, otherwise if not found initializes a new instance with given conds.
 // Each conds must be a struct or map.
+//
+// FirstOrInit never modifies the database. It is often used with Assign and Attrs.
+//
+//	// assign an email if the record is not found
+//	db.Where(User{Name: "non_existing"}).Attrs(User{Email: "fake@fake.org"}).FirstOrInit(&user)
+//	// user -> User{Name: "non_existing", Email: "fake@fake.org"}
+//
+//	// assign email regardless of if record is found
+//	db.Where(User{Name: "jinzhu"}).Assign(User{Email: "fake@fake.org"}).FirstOrInit(&user)
+//	// user -> User{Name: "jinzhu", Age: 20, Email: "fake@fake.org"}
 func (db *DB) FirstOrInit(dest interface{}, conds ...interface{}) (tx *DB) {
 	queryTx := db.Limit(1).Order(clause.OrderByColumn{
 		Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
@@ -317,50 +331,65 @@ func (db *DB) FirstOrInit(dest interface{}, conds ...interface{}) (tx *DB) {
 
 // FirstOrCreate finds the first matching record, otherwise if not found creates a new instance with given conds.
 // Each conds must be a struct or map.
+//
+// Using FirstOrCreate in conjunction with Assign will result in an update to the database even if the record exists.
+//
+//	// assign an email if the record is not found
+//	result := db.Where(User{Name: "non_existing"}).Attrs(User{Email: "fake@fake.org"}).FirstOrCreate(&user)
+//	// user -> User{Name: "non_existing", Email: "fake@fake.org"}
+//	// result.RowsAffected -> 1
+//
+//	// assign email regardless of if record is found
+//	result := db.Where(User{Name: "jinzhu"}).Assign(User{Email: "fake@fake.org"}).FirstOrCreate(&user)
+//	// user -> User{Name: "jinzhu", Age: 20, Email: "fake@fake.org"}
+//	// result.RowsAffected -> 1
 func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) {
 	tx = db.getInstance()
 	queryTx := db.Session(&Session{}).Limit(1).Order(clause.OrderByColumn{
 		Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
 	})
-	if result := queryTx.Find(dest, conds...); result.Error == nil {
-		if result.RowsAffected == 0 {
-			if c, ok := result.Statement.Clauses["WHERE"]; ok {
-				if where, ok := c.Expression.(clause.Where); ok {
-					result.assignInterfacesToValue(where.Exprs)
-				}
-			}
 
-			// initialize with attrs, conds
-			if len(db.Statement.attrs) > 0 {
-				result.assignInterfacesToValue(db.Statement.attrs...)
-			}
+	result := queryTx.Find(dest, conds...)
+	if result.Error != nil {
+		tx.Error = result.Error
+		return tx
+	}
 
-			// initialize with attrs, conds
-			if len(db.Statement.assigns) > 0 {
-				result.assignInterfacesToValue(db.Statement.assigns...)
+	if result.RowsAffected == 0 {
+		if c, ok := result.Statement.Clauses["WHERE"]; ok {
+			if where, ok := c.Expression.(clause.Where); ok {
+				result.assignInterfacesToValue(where.Exprs)
 			}
+		}
 
-			return tx.Create(dest)
-		} else if len(db.Statement.assigns) > 0 {
-			exprs := tx.Statement.BuildCondition(db.Statement.assigns[0], db.Statement.assigns[1:]...)
-			assigns := map[string]interface{}{}
-			for _, expr := range exprs {
-				if eq, ok := expr.(clause.Eq); ok {
-					switch column := eq.Column.(type) {
-					case string:
-						assigns[column] = eq.Value
-					case clause.Column:
-						assigns[column.Name] = eq.Value
-					default:
-					}
+		// initialize with attrs, conds
+		if len(db.Statement.attrs) > 0 {
+			result.assignInterfacesToValue(db.Statement.attrs...)
+		}
+
+		// initialize with attrs, conds
+		if len(db.Statement.assigns) > 0 {
+			result.assignInterfacesToValue(db.Statement.assigns...)
+		}
+
+		return tx.Create(dest)
+	} else if len(db.Statement.assigns) > 0 {
+		exprs := tx.Statement.BuildCondition(db.Statement.assigns[0], db.Statement.assigns[1:]...)
+		assigns := map[string]interface{}{}
+		for _, expr := range exprs {
+			if eq, ok := expr.(clause.Eq); ok {
+				switch column := eq.Column.(type) {
+				case string:
+					assigns[column] = eq.Value
+				case clause.Column:
+					assigns[column.Name] = eq.Value
 				}
 			}
-
-			return tx.Model(dest).Updates(assigns)
 		}
-	} else {
-		tx.Error = result.Error
+
+		return tx.Model(dest).Updates(assigns)
 	}
+
 	return tx
 }
 
@@ -461,7 +490,7 @@ func (db *DB) Count(count *int64) (tx *DB) {
 	tx.Statement.Dest = count
 	tx = tx.callbacks.Query().Execute(tx)
 
-	if _, ok := db.Statement.Clauses["GROUP BY"]; ok || tx.RowsAffected != 1 {
+	if tx.RowsAffected != 1 {
 		*count = tx.RowsAffected
 	}
 
@@ -514,8 +543,9 @@ func (db *DB) Scan(dest interface{}) (tx *DB) {
 }
 
 // Pluck queries a single column from a model, returning in the slice dest. E.g.:
-//     var ages []int64
-//     db.Model(&users).Pluck("age", &ages)
+//
+//	var ages []int64
+//	db.Model(&users).Pluck("age", &ages)
 func (db *DB) Pluck(column string, dest interface{}) (tx *DB) {
 	tx = db.getInstance()
 	if tx.Statement.Model != nil {
diff --git a/gorm.go b/gorm.go
index 589fc4f..89488b7 100644
--- a/gorm.go
+++ b/gorm.go
@@ -179,7 +179,7 @@ func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
 
 	preparedStmt := &PreparedStmtDB{
 		ConnPool:    db.ConnPool,
-		Stmts:       map[string](*Stmt){},
+		Stmts:       make(map[string]*Stmt),
 		Mux:         &sync.RWMutex{},
 		PreparedSQL: make([]string, 0, 100),
 	}
diff --git a/migrator/migrator.go b/migrator/migrator.go
index 9f8e3db..eafe7bb 100644
--- a/migrator/migrator.go
+++ b/migrator/migrator.go
@@ -8,9 +8,11 @@ import (
 	"reflect"
 	"regexp"
 	"strings"
+	"time"
 
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
+	"gorm.io/gorm/logger"
 	"gorm.io/gorm/schema"
 )
 
@@ -30,6 +32,16 @@ type Config struct {
 	gorm.Dialector
 }
 
+type printSQLLogger struct {
+	logger.Interface
+}
+
+func (l *printSQLLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
+	sql, _ := fc()
+	fmt.Println(sql + ";")
+	l.Interface.Trace(ctx, begin, fc, err)
+}
+
 // GormDataTypeInterface gorm data type interface
 type GormDataTypeInterface interface {
 	GormDBDataType(*gorm.DB, *schema.Field) string
@@ -92,14 +104,19 @@ func (m Migrator) FullDataTypeOf(field *schema.Field) (expr clause.Expr) {
 // AutoMigrate auto migrate values
 func (m Migrator) AutoMigrate(values ...interface{}) error {
 	for _, value := range m.ReorderModels(values, true) {
-		tx := m.DB.Session(&gorm.Session{})
-		if !tx.Migrator().HasTable(value) {
-			if err := tx.Migrator().CreateTable(value); err != nil {
+		queryTx := m.DB.Session(&gorm.Session{})
+		execTx := queryTx
+		if m.DB.DryRun {
+			queryTx.DryRun = false
+			execTx = m.DB.Session(&gorm.Session{Logger: &printSQLLogger{Interface: m.DB.Logger}})
+		}
+		if !queryTx.Migrator().HasTable(value) {
+			if err := execTx.Migrator().CreateTable(value); err != nil {
 				return err
 			}
 		} else {
 			if err := m.RunWithValue(value, func(stmt *gorm.Statement) (errr error) {
-				columnTypes, err := m.DB.Migrator().ColumnTypes(value)
+				columnTypes, err := queryTx.Migrator().ColumnTypes(value)
 				if err != nil {
 					return err
 				}
@@ -117,10 +134,10 @@ func (m Migrator) AutoMigrate(values ...interface{}) error {
 
 					if foundColumn == nil {
 						// not found, add column
-						if err := tx.Migrator().AddColumn(value, dbName); err != nil {
+						if err := execTx.Migrator().AddColumn(value, dbName); err != nil {
 							return err
 						}
-					} else if err := m.DB.Migrator().MigrateColumn(value, field, foundColumn); err != nil {
+					} else if err := execTx.Migrator().MigrateColumn(value, field, foundColumn); err != nil {
 						// found, smart migrate
 						return err
 					}
@@ -129,8 +146,8 @@ func (m Migrator) AutoMigrate(values ...interface{}) error {
 				for _, rel := range stmt.Schema.Relationships.Relations {
 					if !m.DB.Config.DisableForeignKeyConstraintWhenMigrating {
 						if constraint := rel.ParseConstraint(); constraint != nil &&
-							constraint.Schema == stmt.Schema && !tx.Migrator().HasConstraint(value, constraint.Name) {
-							if err := tx.Migrator().CreateConstraint(value, constraint.Name); err != nil {
+							constraint.Schema == stmt.Schema && !queryTx.Migrator().HasConstraint(value, constraint.Name) {
+							if err := execTx.Migrator().CreateConstraint(value, constraint.Name); err != nil {
 								return err
 							}
 						}
@@ -138,16 +155,16 @@ func (m Migrator) AutoMigrate(values ...interface{}) error {
 				}
 
 				for _, chk := range stmt.Schema.ParseCheckConstraints() {
-					if !tx.Migrator().HasConstraint(value, chk.Name) {
-						if err := tx.Migrator().CreateConstraint(value, chk.Name); err != nil {
+					if !queryTx.Migrator().HasConstraint(value, chk.Name) {
+						if err := execTx.Migrator().CreateConstraint(value, chk.Name); err != nil {
 							return err
 						}
 					}
 				}
 
 				for _, idx := range stmt.Schema.ParseIndexes() {
-					if !tx.Migrator().HasIndex(value, idx.Name) {
-						if err := tx.Migrator().CreateIndex(value, idx.Name); err != nil {
+					if !queryTx.Migrator().HasIndex(value, idx.Name) {
+						if err := execTx.Migrator().CreateIndex(value, idx.Name); err != nil {
 							return err
 						}
 					}
@@ -470,17 +487,19 @@ func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnTy
 
 	// check default value
 	if !field.PrimaryKey {
+		currentDefaultNotNull := field.HasDefaultValue && !strings.EqualFold(field.DefaultValue, "NULL")
 		dv, dvNotNull := columnType.DefaultValue()
-		if dvNotNull && field.DefaultValueInterface == nil {
+		if dvNotNull && !currentDefaultNotNull {
 			// defalut value -> null
 			alterColumn = true
-		} else if !dvNotNull && field.DefaultValueInterface != nil {
+		} else if !dvNotNull && currentDefaultNotNull {
 			// null -> default value
 			alterColumn = true
-		} else if dv != field.DefaultValue {
+		} else if (field.GORMDataType != schema.Time && dv != field.DefaultValue) ||
+			(field.GORMDataType == schema.Time && !strings.EqualFold(strings.TrimSuffix(dv, "()"), strings.TrimSuffix(field.DefaultValue, "()"))) {
 			// default value not equal
 			// not both null
-			if !(field.DefaultValueInterface == nil && !dvNotNull) {
+			if currentDefaultNotNull || dvNotNull {
 				alterColumn = true
 			}
 		}
diff --git a/prepare_stmt.go b/prepare_stmt.go
index 7591e53..e09fe81 100644
--- a/prepare_stmt.go
+++ b/prepare_stmt.go
@@ -47,13 +47,12 @@ func (db *PreparedStmtDB) Close() {
 func (db *PreparedStmtDB) Reset() {
 	db.Mux.Lock()
 	defer db.Mux.Unlock()
-	for query, stmt := range db.Stmts {
-		delete(db.Stmts, query)
+
+	for _, stmt := range db.Stmts {
 		go stmt.Close()
 	}
-
 	db.PreparedSQL = make([]string, 0, 100)
-	db.Stmts = map[string](*Stmt){}
+	db.Stmts = make(map[string]*Stmt)
 }
 
 func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) {
@@ -93,7 +92,7 @@ func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransact
 
 	// Reason why cannot lock conn.PrepareContext
 	// suppose the maxopen is 1, g1 is creating record and g2 is querying record.
-	// 1. g1 begin tx, g1 is requeued because of waiting for the system call, now `db.ConnPool` db.numOpen == 1.
+	// 1. g1 begin tx, g1 is requeue because of waiting for the system call, now `db.ConnPool` db.numOpen == 1.
 	// 2. g2 select lock `conn.PrepareContext(ctx, query)`, now db.numOpen == db.maxOpen , wait for release.
 	// 3. g1 tx exec insert, wait for unlock `conn.PrepareContext(ctx, query)` to finish tx and release.
 	stmt, err := conn.PrepareContext(ctx, query)
diff --git a/scan.go b/scan.go
index 0a26ce4..12a7786 100644
--- a/scan.go
+++ b/scan.go
@@ -65,7 +65,6 @@ func (db *DB) scanIntoStruct(rows Rows, reflectValue reflect.Value, values []int
 
 	db.RowsAffected++
 	db.AddError(rows.Scan(values...))
-
 	joinedSchemaMap := make(map[*schema.Field]interface{})
 	for idx, field := range fields {
 		if field == nil {
@@ -241,9 +240,8 @@ func Scan(rows Rows, db *DB, mode ScanMode) {
 		switch reflectValue.Kind() {
 		case reflect.Slice, reflect.Array:
 			var (
-				elem             reflect.Value
-				recyclableStruct = reflect.New(reflectValueType)
-				isArrayKind      = reflectValue.Kind() == reflect.Array
+				elem        reflect.Value
+				isArrayKind = reflectValue.Kind() == reflect.Array
 			)
 
 			if !update || reflectValue.Len() == 0 {
@@ -275,11 +273,7 @@ func Scan(rows Rows, db *DB, mode ScanMode) {
 						}
 					}
 				} else {
-					if isPtr && db.RowsAffected > 0 {
-						elem = reflect.New(reflectValueType)
-					} else {
-						elem = recyclableStruct
-					}
+					elem = reflect.New(reflectValueType)
 				}
 
 				db.scanIntoStruct(rows, elem, values, fields, joinFields)
diff --git a/schema/schema.go b/schema/schema.go
index 9b3d30f..21e71c2 100644
--- a/schema/schema.go
+++ b/schema/schema.go
@@ -230,7 +230,7 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam
 	}
 
 	for _, field := range schema.Fields {
-		if field.HasDefaultValue && field.DefaultValueInterface == nil {
+		if field.DataType != "" && field.HasDefaultValue && field.DefaultValueInterface == nil {
 			schema.FieldsWithDefaultDBValue = append(schema.FieldsWithDefaultDBValue, field)
 		}
 	}
diff --git a/statement.go b/statement.go
index d4d20cb..9f49d58 100644
--- a/statement.go
+++ b/statement.go
@@ -49,11 +49,12 @@ type Statement struct {
 }
 
 type join struct {
-	Name    string
-	Conds   []interface{}
-	On      *clause.Where
-	Selects []string
-	Omits   []string
+	Name     string
+	Conds    []interface{}
+	On       *clause.Where
+	Selects  []string
+	Omits    []string
+	JoinType clause.JoinType
 }
 
 // StatementModifier statement modifier interface
diff --git a/utils/utils.go b/utils/utils.go
index 90b4c8e..e08533c 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -3,8 +3,8 @@ package utils
 import (
 	"database/sql/driver"
 	"fmt"
+	"path/filepath"
 	"reflect"
-	"regexp"
 	"runtime"
 	"strconv"
 	"strings"
@@ -16,7 +16,18 @@ var gormSourceDir string
 func init() {
 	_, file, _, _ := runtime.Caller(0)
 	// compatible solution to get gorm source directory with various operating systems
-	gormSourceDir = regexp.MustCompile(`gorm.utils.utils\.go`).ReplaceAllString(file, "")
+	gormSourceDir = sourceDir(file)
+}
+
+func sourceDir(file string) string {
+	dir := filepath.Dir(file)
+	dir = filepath.Dir(dir)
+
+	s := filepath.Dir(dir)
+	if filepath.Base(s) != "gorm.io" {
+		s = dir
+	}
+	return filepath.ToSlash(s) + "/"
 }
 
 // FileWithLineNum return the file name and line number of the current file


Test patch:
diff --git a/tests/associations_belongs_to_test.go b/tests/associations_belongs_to_test.go
index f74799c..a1f014d 100644
--- a/tests/associations_belongs_to_test.go
+++ b/tests/associations_belongs_to_test.go
@@ -3,6 +3,7 @@ package tests_test
 import (
 	"testing"
 
+	"gorm.io/gorm"
 	. "gorm.io/gorm/utils/tests"
 )
 
@@ -224,3 +225,28 @@ func TestBelongsToAssociationForSlice(t *testing.T) {
 	AssertAssociationCount(t, users[0], "Company", 0, "After Delete")
 	AssertAssociationCount(t, users[1], "Company", 1, "After other user Delete")
 }
+
+func TestBelongsToDefaultValue(t *testing.T) {
+	type Org struct {
+		ID string
+	}
+	type BelongsToUser struct {
+		OrgID string
+		Org   Org `gorm:"default:NULL"`
+	}
+
+	tx := DB.Session(&gorm.Session{})
+	tx.Config.DisableForeignKeyConstraintWhenMigrating = true
+	AssertEqual(t, DB.Config.DisableForeignKeyConstraintWhenMigrating, false)
+
+	tx.Migrator().DropTable(&BelongsToUser{}, &Org{})
+	tx.AutoMigrate(&BelongsToUser{}, &Org{})
+
+	user := &BelongsToUser{
+		Org: Org{
+			ID: "BelongsToUser_Org_1",
+		},
+	}
+	err := DB.Create(&user).Error
+	AssertEqual(t, err, nil)
+}
diff --git a/tests/count_test.go b/tests/count_test.go
index b71e3de..2199dc6 100644
--- a/tests/count_test.go
+++ b/tests/count_test.go
@@ -141,7 +141,7 @@ func TestCount(t *testing.T) {
 	}
 	DB.Create(sameUsers)
 
-	if err := DB.Model(&User{}).Where("name = ?", "count-4").Group("name").Count(&count11).Error; err != nil || count11 != 1 {
+	if err := DB.Model(&User{}).Where("name = ?", "count-4").Group("name").Count(&count11).Error; err != nil || count11 != int64(len(sameUsers)) {
 		t.Fatalf("Count should be 3, but got count: %v err %v", count11, err)
 	}
 
diff --git a/tests/embedded_struct_test.go b/tests/embedded_struct_test.go
index e309d06..ae69bac 100644
--- a/tests/embedded_struct_test.go
+++ b/tests/embedded_struct_test.go
@@ -36,7 +36,7 @@ func TestEmbeddedStruct(t *testing.T) {
 
 	type EngadgetPost struct {
 		BasePost BasePost `gorm:"Embedded"`
-		Author   Author   `gorm:"Embedded;EmbeddedPrefix:author_"` // Embedded struct
+		Author   *Author  `gorm:"Embedded;EmbeddedPrefix:author_"` // Embedded struct
 		ImageUrl string
 	}
 
@@ -74,13 +74,27 @@ func TestEmbeddedStruct(t *testing.T) {
 		t.Errorf("embedded struct's value should be scanned correctly")
 	}
 
-	DB.Save(&EngadgetPost{BasePost: BasePost{Title: "engadget_news"}})
+	DB.Save(&EngadgetPost{BasePost: BasePost{Title: "engadget_news"}, Author: &Author{Name: "Edward"}})
+	DB.Save(&EngadgetPost{BasePost: BasePost{Title: "engadget_article"}, Author: &Author{Name: "George"}})
 	var egNews EngadgetPost
 	if err := DB.First(&egNews, "title = ?", "engadget_news").Error; err != nil {
 		t.Errorf("no error should happen when query with embedded struct, but got %v", err)
 	} else if egNews.BasePost.Title != "engadget_news" {
 		t.Errorf("embedded struct's value should be scanned correctly")
 	}
+
+	var egPosts []EngadgetPost
+	if err := DB.Order("author_name asc").Find(&egPosts).Error; err != nil {
+		t.Fatalf("no error should happen when query with embedded struct, but got %v", err)
+	}
+	expectAuthors := []string{"Edward", "George"}
+	for i, post := range egPosts {
+		t.Log(i, post.Author)
+		if want := expectAuthors[i]; post.Author.Name != want {
+			t.Errorf("expected author %s got %s", want, post.Author.Name)
+		}
+	}
+
 }
 
 func TestEmbeddedPointerTypeStruct(t *testing.T) {
diff --git a/tests/go.mod b/tests/go.mod
index 9c87ca3..23fc2ca 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -6,13 +6,13 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/jinzhu/now v1.1.5
 	github.com/lib/pq v1.10.7
-	golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
-	golang.org/x/text v0.4.0 // indirect
-	gorm.io/driver/mysql v1.4.3
-	gorm.io/driver/postgres v1.4.4
+	github.com/mattn/go-sqlite3 v1.14.16 // indirect
+	golang.org/x/crypto v0.3.0 // indirect
+	gorm.io/driver/mysql v1.4.4
+	gorm.io/driver/postgres v1.4.5
 	gorm.io/driver/sqlite v1.4.3
 	gorm.io/driver/sqlserver v1.4.1
-	gorm.io/gorm v1.24.0
+	gorm.io/gorm v1.24.2
 )
 
 replace gorm.io/gorm => ../
diff --git a/tests/joins_test.go b/tests/joins_test.go
index 091fb98..057ad33 100644
--- a/tests/joins_test.go
+++ b/tests/joins_test.go
@@ -230,6 +230,28 @@ func TestJoinWithSoftDeleted(t *testing.T) {
 	}
 }
 
+func TestInnerJoins(t *testing.T) {
+	user := *GetUser("inner-joins-1", Config{Company: true, Manager: true, Account: true, NamedPet: false})
+
+	DB.Create(&user)
+
+	var user2 User
+	var err error
+	err = DB.InnerJoins("Company").InnerJoins("Manager").InnerJoins("Account").First(&user2, "users.name = ?", user.Name).Error
+	AssertEqual(t, err, nil)
+	CheckUser(t, user2, user)
+
+	// inner join and NamedPet is nil
+	err = DB.InnerJoins("NamedPet").InnerJoins("Company").InnerJoins("Manager").InnerJoins("Account").First(&user2, "users.name = ?", user.Name).Error
+	AssertEqual(t, err, gorm.ErrRecordNotFound)
+
+	// mixed inner join and left join
+	var user3 User
+	err = DB.Joins("NamedPet").InnerJoins("Company").InnerJoins("Manager").InnerJoins("Account").First(&user3, "users.name = ?", user.Name).Error
+	AssertEqual(t, err, nil)
+	CheckUser(t, user3, user)
+}
+
 func TestJoinWithSameColumnName(t *testing.T) {
 	user := GetUser("TestJoinWithSameColumnName", Config{
 		Languages: 1,
diff --git a/tests/migrate_test.go b/tests/migrate_test.go
index 96b1d0e..9df626f 100644
--- a/tests/migrate_test.go
+++ b/tests/migrate_test.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"gorm.io/driver/postgres"
-
 	"gorm.io/gorm"
 	"gorm.io/gorm/logger"
 	"gorm.io/gorm/schema"
@@ -29,7 +28,7 @@ func TestMigrate(t *testing.T) {
 	}
 
 	if err := DB.AutoMigrate(allModels...); err != nil {
-		t.Fatalf("Failed to auto migrate, but got error %v", err)
+		t.Fatalf("Failed to auto migrate, got error %v", err)
 	}
 
 	if tables, err := DB.Migrator().GetTables(); err != nil {
@@ -757,6 +756,32 @@ func TestPrimarykeyID(t *testing.T) {
 	}
 }
 
+func TestCurrentTimestamp(t *testing.T) {
+	if DB.Dialector.Name() != "mysql" {
+		return
+	}
+	type CurrentTimestampTest struct {
+		ID     string     `gorm:"primary_key"`
+		TimeAt *time.Time `gorm:"type:datetime;not null;default:CURRENT_TIMESTAMP;unique"`
+	}
+	var err error
+	err = DB.Migrator().DropTable(&CurrentTimestampTest{})
+	if err != nil {
+		t.Errorf("DropTable err:%v", err)
+	}
+	err = DB.AutoMigrate(&CurrentTimestampTest{})
+	if err != nil {
+		t.Fatalf("AutoMigrate err:%v", err)
+	}
+
+	err = DB.AutoMigrate(&CurrentTimestampTest{})
+	if err != nil {
+		t.Fatalf("AutoMigrate err:%v", err)
+	}
+	AssertEqual(t, true, DB.Migrator().HasIndex(&CurrentTimestampTest{}, "time_at"))
+	AssertEqual(t, false, DB.Migrator().HasIndex(&CurrentTimestampTest{}, "time_at_2"))
+}
+
 func TestUniqueColumn(t *testing.T) {
 	if DB.Dialector.Name() != "mysql" {
 		return
@@ -1097,6 +1122,50 @@ func TestMigrateArrayTypeModel(t *testing.T) {
 	AssertEqual(t, "integer[]", ct.DatabaseTypeName())
 }
 
+type mockMigrator struct {
+	gorm.Migrator
+}
+
+func (mm mockMigrator) AlterColumn(dst interface{}, field string) error {
+	err := mm.Migrator.AlterColumn(dst, field)
+	if err != nil {
+		return err
+	}
+	return fmt.Errorf("trigger alter column error, field: %s", field)
+}
+
+func TestMigrateDonotAlterColumn(t *testing.T) {
+	var wrapMockMigrator = func(m gorm.Migrator) mockMigrator {
+		return mockMigrator{
+			Migrator: m,
+		}
+	}
+	m := DB.Migrator()
+	mockM := wrapMockMigrator(m)
+
+	type NotTriggerUpdate struct {
+		ID  uint
+		F1  uint16
+		F2  uint32
+		F3  int
+		F4  int64
+		F5  string
+		F6  float32
+		F7  float64
+		F8  time.Time
+		F9  bool
+		F10 []byte
+	}
+
+	var err error
+	err = mockM.DropTable(&NotTriggerUpdate{})
+	AssertEqual(t, err, nil)
+	err = mockM.AutoMigrate(&NotTriggerUpdate{})
+	AssertEqual(t, err, nil)
+	err = mockM.AutoMigrate(&NotTriggerUpdate{})
+	AssertEqual(t, err, nil)
+}
+
 func TestMigrateSameEmbeddedFieldName(t *testing.T) {
 	type UserStat struct {
 		GroundDestroyCount int
diff --git a/tests/query_test.go b/tests/query_test.go
index eccf013..fa8f09e 100644
--- a/tests/query_test.go
+++ b/tests/query_test.go
@@ -408,6 +408,13 @@ func TestFindInBatchesWithError(t *testing.T) {
 	if totalBatch != 0 {
 		t.Fatalf("incorrect total batch, expected: %v, got: %v", 0, totalBatch)
 	}
+
+	if result := DB.Omit("id").Where("name = ?", users[0].Name).FindInBatches(&results, 2, func(tx *gorm.DB, batch int) error {
+		totalBatch += batch
+		return nil
+	}); result.Error != gorm.ErrPrimaryKeyRequired {
+		t.Fatal("expected errors to have occurred, but nothing happened")
+	}
 }
 
 func TestFillSmallerStruct(t *testing.T) {
diff --git a/utils/utils_test.go b/utils/utils_test.go
index 27dfee1..71eef96 100644
--- a/utils/utils_test.go
+++ b/utils/utils_test.go
@@ -1,8 +1,13 @@
 package utils
 
 import (
+	"database/sql"
+	"database/sql/driver"
+	"errors"
+	"math"
 	"strings"
 	"testing"
+	"time"
 )
 
 func TestIsValidDBNameChar(t *testing.T) {
@@ -13,6 +18,29 @@ func TestIsValidDBNameChar(t *testing.T) {
 	}
 }
 
+func TestCheckTruth(t *testing.T) {
+	checkTruthTests := []struct {
+		v   string
+		out bool
+	}{
+		{"123", true},
+		{"true", true},
+		{"", false},
+		{"false", false},
+		{"False", false},
+		{"FALSE", false},
+		{"\u0046alse", false},
+	}
+
+	for _, test := range checkTruthTests {
+		t.Run(test.v, func(t *testing.T) {
+			if out := CheckTruth(test.v); out != test.out {
+				t.Errorf("CheckTruth(%s) want: %t, got: %t", test.v, test.out, out)
+			}
+		})
+	}
+}
+
 func TestToStringKey(t *testing.T) {
 	cases := []struct {
 		values []interface{}
@@ -29,3 +57,81 @@ func TestToStringKey(t *testing.T) {
 		}
 	}
 }
+
+func TestContains(t *testing.T) {
+	containsTests := []struct {
+		name  string
+		elems []string
+		elem  string
+		out   bool
+	}{
+		{"exists", []string{"1", "2", "3"}, "1", true},
+		{"not exists", []string{"1", "2", "3"}, "4", false},
+	}
+	for _, test := range containsTests {
+		t.Run(test.name, func(t *testing.T) {
+			if out := Contains(test.elems, test.elem); test.out != out {
+				t.Errorf("Contains(%v, %s) want: %t, got: %t", test.elems, test.elem, test.out, out)
+			}
+		})
+	}
+}
+
+type ModifyAt sql.NullTime
+
+// Value return a Unix time.
+func (n ModifyAt) Value() (driver.Value, error) {
+	if !n.Valid {
+		return nil, nil
+	}
+	return n.Time.Unix(), nil
+}
+
+func TestAssertEqual(t *testing.T) {
+	now := time.Now()
+	assertEqualTests := []struct {
+		name     string
+		src, dst interface{}
+		out      bool
+	}{
+		{"error equal", errors.New("1"), errors.New("1"), true},
+		{"error not equal", errors.New("1"), errors.New("2"), false},
+		{"driver.Valuer equal", ModifyAt{Time: now, Valid: true}, ModifyAt{Time: now, Valid: true}, true},
+		{"driver.Valuer not equal", ModifyAt{Time: now, Valid: true}, ModifyAt{Time: now.Add(time.Second), Valid: true}, false},
+	}
+	for _, test := range assertEqualTests {
+		t.Run(test.name, func(t *testing.T) {
+			if out := AssertEqual(test.src, test.dst); test.out != out {
+				t.Errorf("AssertEqual(%v, %v) want: %t, got: %t", test.src, test.dst, test.out, out)
+			}
+		})
+	}
+}
+
+func TestToString(t *testing.T) {
+	tests := []struct {
+		name string
+		in   interface{}
+		out  string
+	}{
+		{"int", math.MaxInt64, "9223372036854775807"},
+		{"int8", int8(math.MaxInt8), "127"},
+		{"int16", int16(math.MaxInt16), "32767"},
+		{"int32", int32(math.MaxInt32), "2147483647"},
+		{"int64", int64(math.MaxInt64), "9223372036854775807"},
+		{"uint", uint(math.MaxUint64), "18446744073709551615"},
+		{"uint8", uint8(math.MaxUint8), "255"},
+		{"uint16", uint16(math.MaxUint16), "65535"},
+		{"uint32", uint32(math.MaxUint32), "4294967295"},
+		{"uint64", uint64(math.MaxUint64), "18446744073709551615"},
+		{"string", "abc", "abc"},
+		{"other", true, ""},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if out := ToString(test.in); test.out != out {
+				t.Fatalf("ToString(%v) want: %s, got: %s", test.in, test.out, out)
+			}
+		})
+	}
+}
diff --git a/utils/utils_unix_test.go b/utils/utils_unix_test.go
new file mode 100644
index 0000000..450cbe2
--- /dev/null
+++ b/utils/utils_unix_test.go
@@ -0,0 +1,38 @@
+//go:build unix
+// +build unix
+
+package utils
+
+import (
+	"testing"
+)
+
+func TestSourceDir(t *testing.T) {
+	cases := []struct {
+		file string
+		want string
+	}{
+		{
+			file: "/Users/name/go/pkg/mod/gorm.io/gorm@v1.2.3/utils/utils.go",
+			want: "/Users/name/go/pkg/mod/gorm.io/",
+		},
+		{
+			file: "/go/work/proj/gorm/utils/utils.go",
+			want: "/go/work/proj/gorm/",
+		},
+		{
+			file: "/go/work/proj/gorm_alias/utils/utils.go",
+			want: "/go/work/proj/gorm_alias/",
+		},
+		{
+			file: "/go/work/proj/my.gorm.io/gorm@v1.2.3/utils/utils.go",
+			want: "/go/work/proj/my.gorm.io/gorm@v1.2.3/",
+		},
+	}
+	for _, c := range cases {
+		s := sourceDir(c.file)
+		if s != c.want {
+			t.Fatalf("%s: expected %s, got %s", c.file, c.want, s)
+		}
+	}
+}
diff --git a/utils/utils_windows_test.go b/utils/utils_windows_test.go
new file mode 100644
index 0000000..8b1c519
--- /dev/null
+++ b/utils/utils_windows_test.go
@@ -0,0 +1,35 @@
+package utils
+
+import (
+	"testing"
+)
+
+func TestSourceDir(t *testing.T) {
+	cases := []struct {
+		file string
+		want string
+	}{
+		{
+			file: `C:/Users/name/go/pkg/mod/gorm.io/gorm@v1.2.3/utils/utils.go`,
+			want: `C:/Users/name/go/pkg/mod/gorm.io/`,
+		},
+		{
+			file: `C:/go/work/proj/gorm/utils/utils.go`,
+			want: `C:/go/work/proj/gorm/`,
+		},
+		{
+			file: `C:/go/work/proj/gorm_alias/utils/utils.go`,
+			want: `C:/go/work/proj/gorm_alias/`,
+		},
+		{
+			file: `C:/go/work/proj/my.gorm.io/gorm@v1.2.3/utils/utils.go`,
+			want: `C:/go/work/proj/my.gorm.io/gorm@v1.2.3/`,
+		},
+	}
+	for _, c := range cases {
+		s := sourceDir(c.file)
+		if s != c.want {
+			t.Fatalf("%s: expected %s, got %s", c.file, c.want, s)
+		}
+	}
+}