Repository: go-gorm/gorm
Pull request #6917 (2024-03-21 03:34:09)

fix: 'type XXXX int' will print wrong sql to terminal
<!--
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?
ref: https://github.com/go-gorm/gorm/issues/6898

The wrong sql printed to the terminal can be confusing when use `type XXX int`, so fix it.

<!--
provide a general description of the code changes in your pull request
-->

### User Case Description

<!-- Your use case -->

```go
type UserStatus int

const (
	Normal UserStatus = 1
	Forbid UserStatus = 2
)

DB.Where("status = ?", Normal).Find(&users)
```
It print: 
``` go
SELECT * FROM `users` WHERE status = '1' AND `users`.`deleted_at` IS NULL
```

The expected print is: 
``` go 
SELECT * FROM `users` WHERE status = 1 AND `users`.`deleted_at` IS NULL
```



Patch:
diff --git a/logger/sql.go b/logger/sql.go
index 8ce8d8b..ad47879 100644
--- a/logger/sql.go
+++ b/logger/sql.go
@@ -34,6 +34,19 @@ var convertibleTypes = []reflect.Type{reflect.TypeOf(time.Time{}), reflect.TypeO
 // RegEx matches only numeric values
 var numericPlaceholderRe = regexp.MustCompile(`\$\d+\$`)
 
+func isNumeric(k reflect.Kind) bool {
+	switch k {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return true
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return true
+	case reflect.Float32, reflect.Float64:
+		return true
+	default:
+		return false
+	}
+}
+
 // ExplainSQL generate SQL string with given parameters, the generated SQL is expected to be used in logger, execute it might introduce a SQL injection vulnerability
 func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, avars ...interface{}) string {
 	var (
@@ -110,6 +123,12 @@ func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, a
 				convertParams(v, idx)
 			} else if rv.Kind() == reflect.Ptr && !rv.IsZero() {
 				convertParams(reflect.Indirect(rv).Interface(), idx)
+			} else if isNumeric(rv.Kind()) {
+				if rv.CanInt() || rv.CanUint() {
+					vars[idx] = fmt.Sprintf("%d", rv.Interface())
+				} else {
+					vars[idx] = fmt.Sprintf("%.6f", rv.Interface())
+				}
 			} else {
 				for _, t := range convertibleTypes {
 					if rv.Type().ConvertibleTo(t) {


Test patch:
diff --git a/logger/sql_test.go b/logger/sql_test.go
index 036ef3a..9002a7e 100644
--- a/logger/sql_test.go
+++ b/logger/sql_test.go
@@ -37,14 +37,18 @@ func format(v []byte, escaper string) string {
 func TestExplainSQL(t *testing.T) {
 	type role string
 	type password []byte
+	type intType int
+	type floatType float64
 	var (
-		tt     = now.MustParse("2020-02-23 11:10:10")
-		myrole = role("admin")
-		pwd    = password("pass")
-		jsVal  = []byte(`{"Name":"test","Val":"test"}`)
-		js     = JSON(jsVal)
-		esVal  = []byte(`{"Name":"test","Val":"test"}`)
-		es     = ExampleStruct{Name: "test", Val: "test"}
+		tt                 = now.MustParse("2020-02-23 11:10:10")
+		myrole             = role("admin")
+		pwd                = password("pass")
+		jsVal              = []byte(`{"Name":"test","Val":"test"}`)
+		js                 = JSON(jsVal)
+		esVal              = []byte(`{"Name":"test","Val":"test"}`)
+		es                 = ExampleStruct{Name: "test", Val: "test"}
+		intVal   intType   = 1
+		floatVal floatType = 1.23
 	)
 
 	results := []struct {
@@ -107,6 +111,18 @@ func TestExplainSQL(t *testing.T) {
 			Vars:          []interface{}{"jinzhu", 1, float32(999.99), true, []byte("12345"), tt, &tt, nil, "w@g.\"com", myrole, pwd, &js, &es},
 			Result:        fmt.Sprintf(`create table users (name, age, height, actived, bytes, create_at, update_at, deleted_at, email, role, pass, json_struct, example_struct) values ("jinzhu", 1, 999.99, true, "12345", "2020-02-23 11:10:10", "2020-02-23 11:10:10", NULL, "w@g.""com", "admin", "pass", %v, %v)`, format(jsVal, `"`), format(esVal, `"`)),
 		},
+		{
+			SQL:           "create table users (name, age, height, actived, bytes, create_at, update_at, deleted_at, email, role, pass, int_val) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+			NumericRegexp: nil,
+			Vars:          []interface{}{"jinzhu?", 1, 999.99, true, []byte("12345"), tt, &tt, nil, "w@g.\"com", myrole, pwd, intVal},
+			Result:        `create table users (name, age, height, actived, bytes, create_at, update_at, deleted_at, email, role, pass, int_val) values ("jinzhu?", 1, 999.99, true, "12345", "2020-02-23 11:10:10", "2020-02-23 11:10:10", NULL, "w@g.""com", "admin", "pass", 1)`,
+		},
+		{
+			SQL:           "create table users (name, age, height, actived, bytes, create_at, update_at, deleted_at, email, role, pass, float_val) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+			NumericRegexp: nil,
+			Vars:          []interface{}{"jinzhu?", 1, 999.99, true, []byte("12345"), tt, &tt, nil, "w@g.\"com", myrole, pwd, floatVal},
+			Result:        `create table users (name, age, height, actived, bytes, create_at, update_at, deleted_at, email, role, pass, float_val) values ("jinzhu?", 1, 999.99, true, "12345", "2020-02-23 11:10:10", "2020-02-23 11:10:10", NULL, "w@g.""com", "admin", "pass", 1.230000)`,
+		},
 	}
 
 	for idx, r := range results {
diff --git a/tests/go.mod b/tests/go.mod
index 5616ebb..3d3901d 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/jinzhu/now v1.1.5
 	github.com/lib/pq v1.10.9
 	github.com/stretchr/testify v1.9.0
-	gorm.io/driver/mysql v1.5.5
+	gorm.io/driver/mysql v1.5.6
 	gorm.io/driver/postgres v1.5.7
 	gorm.io/driver/sqlite v1.5.5
 	gorm.io/driver/sqlserver v1.5.3
diff --git a/tests/prepared_stmt_test.go b/tests/prepared_stmt_test.go
index b234c8b..b86bc3d 100644
--- a/tests/prepared_stmt_test.go
+++ b/tests/prepared_stmt_test.go
@@ -126,33 +126,6 @@ func TestPreparedStmtDeadlock(t *testing.T) {
 	AssertEqual(t, sqlDB.Stats().InUse, 0)
 }
 
-func TestPreparedStmtError(t *testing.T) {
-	tx, err := OpenTestConnection(&gorm.Config{})
-	AssertEqual(t, err, nil)
-
-	sqlDB, _ := tx.DB()
-	sqlDB.SetMaxOpenConns(1)
-
-	tx = tx.Session(&gorm.Session{PrepareStmt: true})
-
-	wg := sync.WaitGroup{}
-	for i := 0; i < 10; i++ {
-		wg.Add(1)
-		go func() {
-			// err prepare
-			tag := Tag{Locale: "zh"}
-			tx.Table("users").Find(&tag)
-			wg.Done()
-		}()
-	}
-	wg.Wait()
-
-	conn, ok := tx.ConnPool.(*gorm.PreparedStmtDB)
-	AssertEqual(t, ok, true)
-	AssertEqual(t, len(conn.Stmts), 0)
-	AssertEqual(t, sqlDB.Stats().InUse, 0)
-}
-
 func TestPreparedStmtInTransaction(t *testing.T) {
 	user := User{Name: "jinzhu"}