Repository: go-gorm/gorm
Pull request #6014 (2023-02-01 07:12:23)

feat: add tidb integration test cases
<!--
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?

Add TiDB integration test cases. And skip some features that TiDB is not implicated.
This PR is based on https://github.com/go-gorm/mysql/pull/105, need to wait for that to merge.

<!--
provide a general description of the code changes in your pull request
-->

### User Case Description

<!-- Your use case -->


Icemap: This PR request https://github.com/go-gorm/mysql/pull/105 merged. Please hold until it's merged.

Patch:
diff --git a/callbacks.go b/callbacks.go
index ebebf79..de979e4 100644
--- a/callbacks.go
+++ b/callbacks.go
@@ -93,6 +93,10 @@ func (p *processor) Execute(db *DB) *DB {
 		resetBuildClauses = true
 	}
 
+	if optimizer, ok := db.Statement.Dest.(StatementModifier); ok {
+		optimizer.ModifyStatement(stmt)
+	}
+
 	// assign model values
 	if stmt.Model == nil {
 		stmt.Model = stmt.Dest
diff --git a/callbacks/row.go b/callbacks/row.go
index 56be742..beaa189 100644
--- a/callbacks/row.go
+++ b/callbacks/row.go
@@ -7,7 +7,7 @@ import (
 func RowQuery(db *gorm.DB) {
 	if db.Error == nil {
 		BuildQuerySQL(db)
-		if db.DryRun {
+		if db.DryRun || db.Error != nil {
 			return
 		}
 
diff --git a/clause/clause.go b/clause/clause.go
index de19f2e..1354fc0 100644
--- a/clause/clause.go
+++ b/clause/clause.go
@@ -20,6 +20,7 @@ type Builder interface {
 	Writer
 	WriteQuoted(field interface{})
 	AddVar(Writer, ...interface{})
+	AddError(error) error
 }
 
 // Clause
diff --git a/errors.go b/errors.go
index 49cbfe6..0f486c5 100644
--- a/errors.go
+++ b/errors.go
@@ -21,6 +21,8 @@ var (
 	ErrPrimaryKeyRequired = errors.New("primary key required")
 	// ErrModelValueRequired model value required
 	ErrModelValueRequired = errors.New("model value required")
+	// ErrModelAccessibleFieldsRequired model accessible fields required
+	ErrModelAccessibleFieldsRequired = errors.New("model accessible fields required")
 	// ErrInvalidData unsupported data
 	ErrInvalidData = errors.New("unsupported data")
 	// ErrUnsupportedDriver unsupported driver
diff --git a/statement.go b/statement.go
index b99648f..bc959f0 100644
--- a/statement.go
+++ b/statement.go
@@ -120,6 +120,8 @@ func (stmt *Statement) QuoteTo(writer clause.Writer, field interface{}) {
 				write(v.Raw, stmt.Schema.PrioritizedPrimaryField.DBName)
 			} else if len(stmt.Schema.DBNames) > 0 {
 				write(v.Raw, stmt.Schema.DBNames[0])
+			} else {
+				stmt.DB.AddError(ErrModelAccessibleFieldsRequired) //nolint:typecheck,errcheck
 			}
 		} else {
 			write(v.Raw, v.Name)
@@ -311,6 +313,9 @@ func (stmt *Statement) BuildCondition(query interface{}, args ...interface{}) []
 	conds := make([]clause.Expression, 0, 4)
 	args = append([]interface{}{query}, args...)
 	for idx, arg := range args {
+		if arg == nil {
+			continue
+		}
 		if valuer, ok := arg.(driver.Valuer); ok {
 			arg, _ = valuer.Value()
 		}


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5e9a1e6..cfe8e56 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -167,3 +167,36 @@ jobs:
 
     - name: Tests
       run: GITHUB_ACTION=true GORM_DIALECT=sqlserver GORM_DSN="sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" ./tests/tests_all.sh
+
+  tidb:
+    strategy:
+      matrix:
+        dbversion: [ 'v6.5.0' ]
+        go: [ '1.19', '1.18' ]
+        platform: [ ubuntu-latest ]
+    runs-on: ${{ matrix.platform }}
+
+    steps:
+      - name: Setup TiDB
+        uses: Icemap/tidb-action@main
+        with:
+          port: 9940
+          version: ${{matrix.dbversion}}
+
+      - name: Set up Go 1.x
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.go }}
+
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v3
+
+
+      - name: go mod package cache
+        uses: actions/cache@v3
+        with:
+          path: ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('tests/go.mod') }}
+
+      - name: Tests
+        run: GITHUB_ACTION=true GORM_DIALECT=tidb GORM_DSN="root:@tcp(localhost:9940)/test?charset=utf8&parseTime=True&loc=Local" ./tests/tests_all.sh
diff --git a/statement_test.go b/statement_test.go
index 761daf3..648bc87 100644
--- a/statement_test.go
+++ b/statement_test.go
@@ -35,6 +35,13 @@ func TestWhereCloneCorruption(t *testing.T) {
 	}
 }
 
+func TestNilCondition(t *testing.T) {
+	s := new(Statement)
+	if len(s.BuildCondition(nil)) != 0 {
+		t.Errorf("Nil condition should be empty")
+	}
+}
+
 func TestNameMatcher(t *testing.T) {
 	for k, v := range map[string][]string{
 		"table.name":         {"table", "name"},
diff --git a/tests/associations_belongs_to_test.go b/tests/associations_belongs_to_test.go
index a1f014d..99e8aa7 100644
--- a/tests/associations_belongs_to_test.go
+++ b/tests/associations_belongs_to_test.go
@@ -138,6 +138,7 @@ func TestBelongsToAssociation(t *testing.T) {
 	unexistCompanyID := company.ID + 9999999
 	user = User{Name: "invalid-user-with-invalid-belongs-to-foreign-key", CompanyID: &unexistCompanyID}
 	if err := DB.Create(&user).Error; err == nil {
+		tidbSkip(t, "not support the foreign key feature")
 		t.Errorf("should have gotten foreign key violation error")
 	}
 }
diff --git a/tests/associations_many2many_test.go b/tests/associations_many2many_test.go
index 7b45bef..4ba31f9 100644
--- a/tests/associations_many2many_test.go
+++ b/tests/associations_many2many_test.go
@@ -95,6 +95,8 @@ func TestMany2ManyAssociation(t *testing.T) {
 }
 
 func TestMany2ManyOmitAssociations(t *testing.T) {
+	tidbSkip(t, "not support the foreign key feature")
+
 	user := *GetUser("many2many_omit_associations", Config{Languages: 2})
 
 	if err := DB.Omit("Languages.*").Create(&user).Error; err == nil {
diff --git a/tests/associations_test.go b/tests/associations_test.go
index 4c9076d..4e8862e 100644
--- a/tests/associations_test.go
+++ b/tests/associations_test.go
@@ -71,6 +71,8 @@ func TestAssociationNotNullClear(t *testing.T) {
 }
 
 func TestForeignKeyConstraints(t *testing.T) {
+	tidbSkip(t, "not support the foreign key feature")
+
 	type Profile struct {
 		ID       uint
 		Name     string
@@ -126,6 +128,8 @@ func TestForeignKeyConstraints(t *testing.T) {
 }
 
 func TestForeignKeyConstraintsBelongsTo(t *testing.T) {
+	tidbSkip(t, "not support the foreign key feature")
+
 	type Profile struct {
 		ID    uint
 		Name  string
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 9ab4ddb..0e5673f 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -29,3 +29,8 @@ services:
       - MSSQL_DB=gorm
       - MSSQL_USER=gorm
       - MSSQL_PASSWORD=LoremIpsum86
+  tidb:
+    image: 'pingcap/tidb:v6.5.0'
+    ports:
+      - 9940:4000
+    command: /tidb-server -store unistore -path "" -lease 0s > tidb.log 2>&1 &
diff --git a/tests/go.mod b/tests/go.mod
index 251aabb..69d6cf8 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -8,11 +8,11 @@ require (
 	github.com/lib/pq v1.10.7
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	golang.org/x/crypto v0.5.0 // indirect
-	gorm.io/driver/mysql v1.4.5
+	gorm.io/driver/mysql v1.4.6
 	gorm.io/driver/postgres v1.4.6
 	gorm.io/driver/sqlite v1.4.4
 	gorm.io/driver/sqlserver v1.4.2
-	gorm.io/gorm v1.24.3
+	gorm.io/gorm v1.24.5
 )
 
 replace gorm.io/gorm => ../
diff --git a/tests/helper_test.go b/tests/helper_test.go
index d1af073..d40fa5c 100644
--- a/tests/helper_test.go
+++ b/tests/helper_test.go
@@ -1,6 +1,7 @@
 package tests_test
 
 import (
+	"os"
 	"sort"
 	"strconv"
 	"strings"
@@ -235,3 +236,13 @@ func CheckUser(t *testing.T, user User, expect User) {
 		}
 	})
 }
+
+func tidbSkip(t *testing.T, reason string) {
+	if isTiDB() {
+		t.Skipf("This test case skipped, because of TiDB '%s'", reason)
+	}
+}
+
+func isTiDB() bool {
+	return os.Getenv("GORM_DIALECT") == "tidb"
+}
diff --git a/tests/migrate_test.go b/tests/migrate_test.go
index fcd0b5b..489da97 100644
--- a/tests/migrate_test.go
+++ b/tests/migrate_test.go
@@ -374,7 +374,137 @@ func TestMigrateIndexes(t *testing.T) {
 	}
 }
 
+func TestTiDBMigrateColumns(t *testing.T) {
+	if !isTiDB() {
+		t.Skip()
+	}
+
+	// TiDB can't change column constraint and has auto_random feature
+	type ColumnStruct struct {
+		ID    int `gorm:"primarykey;default:auto_random()"`
+		Name  string
+		Age   int    `gorm:"default:18;comment:my age"`
+		Code  string `gorm:"unique;comment:my code;"`
+		Code2 string
+		Code3 string `gorm:"unique"`
+	}
+
+	DB.Migrator().DropTable(&ColumnStruct{})
+
+	if err := DB.AutoMigrate(&ColumnStruct{}); err != nil {
+		t.Errorf("Failed to migrate, got %v", err)
+	}
+
+	type ColumnStruct2 struct {
+		ID    int    `gorm:"primarykey;default:auto_random()"`
+		Name  string `gorm:"size:100"`
+		Code  string `gorm:"unique;comment:my code2;default:hello"`
+		Code2 string `gorm:"comment:my code2;default:hello"`
+	}
+
+	if err := DB.Table("column_structs").Migrator().AlterColumn(&ColumnStruct{}, "Name"); err != nil {
+		t.Fatalf("no error should happened when alter column, but got %v", err)
+	}
+
+	if err := DB.Table("column_structs").AutoMigrate(&ColumnStruct2{}); err != nil {
+		t.Fatalf("no error should happened when auto migrate column, but got %v", err)
+	}
+
+	if columnTypes, err := DB.Migrator().ColumnTypes(&ColumnStruct{}); err != nil {
+		t.Fatalf("no error should returns for ColumnTypes")
+	} else {
+		stmt := &gorm.Statement{DB: DB}
+		stmt.Parse(&ColumnStruct2{})
+
+		for _, columnType := range columnTypes {
+			switch columnType.Name() {
+			case "id":
+				if v, ok := columnType.PrimaryKey(); !ok || !v {
+					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+				}
+			case "name":
+				dataType := DB.Dialector.DataTypeOf(stmt.Schema.LookUpField(columnType.Name()))
+				if !strings.Contains(strings.ToUpper(dataType), strings.ToUpper(columnType.DatabaseTypeName())) {
+					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
+				}
+				if length, ok := columnType.Length(); !ok || length != 100 {
+					t.Fatalf("column name length should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), length, 100, columnType)
+				}
+			case "age":
+				if v, ok := columnType.DefaultValue(); !ok || v != "18" {
+					t.Fatalf("column age default value should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+				}
+				if v, ok := columnType.Comment(); !ok || v != "my age" {
+					t.Fatalf("column age comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+				}
+			case "code":
+				if v, ok := columnType.Unique(); !ok || !v {
+					t.Fatalf("column code unique should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+				}
+				if v, ok := columnType.DefaultValue(); !ok || v != "hello" {
+					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v", columnType.Name(), columnType, v)
+				}
+				if v, ok := columnType.Comment(); !ok || v != "my code2" {
+					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+				}
+			case "code2":
+				// Code2 string `gorm:"comment:my code2;default:hello"`
+				if v, ok := columnType.DefaultValue(); !ok || v != "hello" {
+					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v", columnType.Name(), columnType, v)
+				}
+				if v, ok := columnType.Comment(); !ok || v != "my code2" {
+					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+				}
+			}
+		}
+	}
+
+	type NewColumnStruct struct {
+		gorm.Model
+		Name    string
+		NewName string
+	}
+
+	if err := DB.Table("column_structs").Migrator().AddColumn(&NewColumnStruct{}, "NewName"); err != nil {
+		t.Fatalf("Failed to add column, got %v", err)
+	}
+
+	if !DB.Table("column_structs").Migrator().HasColumn(&NewColumnStruct{}, "NewName") {
+		t.Fatalf("Failed to find added column")
+	}
+
+	if err := DB.Table("column_structs").Migrator().DropColumn(&NewColumnStruct{}, "NewName"); err != nil {
+		t.Fatalf("Failed to add column, got %v", err)
+	}
+
+	if DB.Table("column_structs").Migrator().HasColumn(&NewColumnStruct{}, "NewName") {
+		t.Fatalf("Found deleted column")
+	}
+
+	if err := DB.Table("column_structs").Migrator().AddColumn(&NewColumnStruct{}, "NewName"); err != nil {
+		t.Fatalf("Failed to add column, got %v", err)
+	}
+
+	if err := DB.Table("column_structs").Migrator().RenameColumn(&NewColumnStruct{}, "NewName", "new_new_name"); err != nil {
+		t.Fatalf("Failed to add column, got %v", err)
+	}
+
+	if !DB.Table("column_structs").Migrator().HasColumn(&NewColumnStruct{}, "new_new_name") {
+		t.Fatalf("Failed to found renamed column")
+	}
+
+	if err := DB.Table("column_structs").Migrator().DropColumn(&NewColumnStruct{}, "new_new_name"); err != nil {
+		t.Fatalf("Failed to add column, got %v", err)
+	}
+
+	if DB.Table("column_structs").Migrator().HasColumn(&NewColumnStruct{}, "new_new_name") {
+		t.Fatalf("Found deleted column")
+	}
+}
+
 func TestMigrateColumns(t *testing.T) {
+	tidbSkip(t, "use another test case")
+
 	sqlite := DB.Dialector.Name() == "sqlite"
 	sqlserver := DB.Dialector.Name() == "sqlserver"
 
@@ -853,6 +983,8 @@ func TestUniqueColumn(t *testing.T) {
 	AssertEqual(t, "", value)
 	AssertEqual(t, false, ok)
 
+	tidbSkip(t, "can't change column constraint")
+
 	// null -> empty string
 	err = DB.Table("unique_tests").AutoMigrate(&UniqueTest3{})
 	if err != nil {
diff --git a/tests/query_test.go b/tests/query_test.go
index fa8f09e..88e93c7 100644
--- a/tests/query_test.go
+++ b/tests/query_test.go
@@ -1366,3 +1366,17 @@ func TestQueryResetNullValue(t *testing.T) {
 	AssertEqual(t, q1, qs[0])
 	AssertEqual(t, q2, qs[1])
 }
+
+func TestQueryError(t *testing.T) {
+	type P struct{}
+	var p1 P
+	err := DB.Take(&p1, 1).Error
+	AssertEqual(t, err, gorm.ErrModelAccessibleFieldsRequired)
+
+	var p2 interface{}
+
+	err = DB.Table("ps").Clauses(clause.Eq{Column: clause.Column{
+		Table: clause.CurrentTable, Name: clause.PrimaryKey,
+	}, Value: 1}).Scan(&p2).Error
+	AssertEqual(t, err, gorm.ErrModelValueRequired)
+}
diff --git a/tests/sql_builder_test.go b/tests/sql_builder_test.go
index 0fbd611..022e049 100644
--- a/tests/sql_builder_test.go
+++ b/tests/sql_builder_test.go
@@ -29,7 +29,7 @@ func TestRow(t *testing.T) {
 	}
 
 	table := "gorm.users"
-	if DB.Dialector.Name() != "mysql" {
+	if DB.Dialector.Name() != "mysql" || isTiDB() {
 		table = "users" // other databases doesn't support select with `database.table`
 	}
 
diff --git a/tests/tests_all.sh b/tests/tests_all.sh
index 5b9bae9..ee9e767 100755
--- a/tests/tests_all.sh
+++ b/tests/tests_all.sh
@@ -1,6 +1,6 @@
 #!/bin/bash -e
 
-dialects=("sqlite" "mysql" "postgres" "sqlserver")
+dialects=("sqlite" "mysql" "postgres" "sqlserver" "tidb")
 
 if [[ $(pwd) == *"gorm/tests"* ]]; then
   cd ..
diff --git a/tests/tests_test.go b/tests/tests_test.go
index dcba3cb..90eb847 100644
--- a/tests/tests_test.go
+++ b/tests/tests_test.go
@@ -21,6 +21,7 @@ var (
 	mysqlDSN     = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local"
 	postgresDSN  = "user=gorm password=gorm dbname=gorm host=localhost port=9920 sslmode=disable TimeZone=Asia/Shanghai"
 	sqlserverDSN = "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
+	tidbDSN      = "root:@tcp(localhost:9940)/test?charset=utf8&parseTime=True&loc=Local"
 )
 
 func init() {
@@ -80,6 +81,12 @@ func OpenTestConnection() (db *gorm.DB, err error) {
 			dbDSN = sqlserverDSN
 		}
 		db, err = gorm.Open(sqlserver.Open(dbDSN), &gorm.Config{})
+	case "tidb":
+		log.Println("testing tidb...")
+		if dbDSN == "" {
+			dbDSN = tidbDSN
+		}
+		db, err = gorm.Open(mysql.Open(dbDSN), &gorm.Config{})
 	default:
 		log.Println("testing sqlite3...")
 		db, err = gorm.Open(sqlite.Open(filepath.Join(os.TempDir(), "gorm.db")), &gorm.Config{})