Repository: go-gorm/gorm
Pull request #6855 (2024-02-28 02:21:58)

fix(create): fix insert column order
<!--
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?
make insert sql column order stable for unittest
<!--
provide a general description of the code changes in your pull request
-->

### User Case Description
gorm create() interface generated sql not stable for each test run. I changed the map interater to a stable slice in order to keep the column order. 


archever: > Can you add some unit tests to describe what is being fixed?

add a ut case

Patch:
diff --git a/callbacks/create.go b/callbacks/create.go
index b1488b0..d930e92 100644
--- a/callbacks/create.go
+++ b/callbacks/create.go
@@ -293,13 +293,15 @@ func ConvertToCreateValues(stmt *gorm.Statement) (values clause.Values) {
 				}
 			}
 
-			for field, vs := range defaultValueFieldsHavingValue {
-				values.Columns = append(values.Columns, clause.Column{Name: field.DBName})
-				for idx := range values.Values {
-					if vs[idx] == nil {
-						values.Values[idx] = append(values.Values[idx], stmt.Dialector.DefaultValueOf(field))
-					} else {
-						values.Values[idx] = append(values.Values[idx], vs[idx])
+			for _, field := range stmt.Schema.FieldsWithDefaultDBValue {
+				if vs, ok := defaultValueFieldsHavingValue[field]; ok {
+					values.Columns = append(values.Columns, clause.Column{Name: field.DBName})
+					for idx := range values.Values {
+						if vs[idx] == nil {
+							values.Values[idx] = append(values.Values[idx], stmt.Dialector.DefaultValueOf(field))
+						} else {
+							values.Values[idx] = append(values.Values[idx], vs[idx])
+						}
 					}
 				}
 			}
@@ -351,7 +353,7 @@ func ConvertToCreateValues(stmt *gorm.Statement) (values clause.Values) {
 									case schema.UnixNanosecond:
 										assignment.Value = curTime.UnixNano()
 									case schema.UnixMillisecond:
-										assignment.Value = curTime.UnixNano() / 1e6
+										assignment.Value = curTime.UnixMilli()
 									case schema.UnixSecond:
 										assignment.Value = curTime.Unix()
 									}
diff --git a/callbacks/preload.go b/callbacks/preload.go
index 25ecfe7..cf7a0d2 100644
--- a/callbacks/preload.go
+++ b/callbacks/preload.go
@@ -121,10 +121,23 @@ func preloadEntryPoint(db *gorm.DB, joins []string, relationships *schema.Relati
 			}
 		} else if rel := relationships.Relations[name]; rel != nil {
 			if joined, nestedJoins := isJoined(name); joined {
-				reflectValue := rel.Field.ReflectValueOf(db.Statement.Context, db.Statement.ReflectValue)
-				tx := preloadDB(db, reflectValue, reflectValue.Interface())
-				if err := preloadEntryPoint(tx, nestedJoins, &tx.Statement.Schema.Relationships, preloadMap[name], associationsConds); err != nil {
-					return err
+				switch rv := db.Statement.ReflectValue; rv.Kind() {
+				case reflect.Slice, reflect.Array:
+					for i := 0; i < rv.Len(); i++ {
+						reflectValue := rel.Field.ReflectValueOf(db.Statement.Context, rv.Index(i))
+						tx := preloadDB(db, reflectValue, reflectValue.Interface())
+						if err := preloadEntryPoint(tx, nestedJoins, &tx.Statement.Schema.Relationships, preloadMap[name], associationsConds); err != nil {
+							return err
+						}
+					}
+				case reflect.Struct:
+					reflectValue := rel.Field.ReflectValueOf(db.Statement.Context, rv)
+					tx := preloadDB(db, reflectValue, reflectValue.Interface())
+					if err := preloadEntryPoint(tx, nestedJoins, &tx.Statement.Schema.Relationships, preloadMap[name], associationsConds); err != nil {
+						return err
+					}
+				default:
+					return gorm.ErrInvalidData
 				}
 			} else {
 				tx := db.Table("").Session(&gorm.Session{Context: db.Statement.Context, SkipHooks: db.Statement.SkipHooks})
diff --git a/callbacks/update.go b/callbacks/update.go
index ff075dc..7cde7f6 100644
--- a/callbacks/update.go
+++ b/callbacks/update.go
@@ -234,7 +234,7 @@ func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) {
 						if field.AutoUpdateTime == schema.UnixNanosecond {
 							set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: now.UnixNano()})
 						} else if field.AutoUpdateTime == schema.UnixMillisecond {
-							set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: now.UnixNano() / 1e6})
+							set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: now.UnixMilli()})
 						} else if field.AutoUpdateTime == schema.UnixSecond {
 							set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: now.Unix()})
 						} else {
@@ -268,7 +268,7 @@ func ConvertToAssignments(stmt *gorm.Statement) (set clause.Set) {
 								if field.AutoUpdateTime == schema.UnixNanosecond {
 									value = stmt.DB.NowFunc().UnixNano()
 								} else if field.AutoUpdateTime == schema.UnixMillisecond {
-									value = stmt.DB.NowFunc().UnixNano() / 1e6
+									value = stmt.DB.NowFunc().UnixMilli()
 								} else if field.AutoUpdateTime == schema.UnixSecond {
 									value = stmt.DB.NowFunc().Unix()
 								} else {
diff --git a/clause/where.go b/clause/where.go
index 46d0b31..9ac7857 100644
--- a/clause/where.go
+++ b/clause/where.go
@@ -21,11 +21,11 @@ func (where Where) Name() string {
 
 // Build build where clause
 func (where Where) Build(builder Builder) {
-    if len(where.Exprs) == 1 {
-        if andCondition, ok := where.Exprs[0].(AndConditions); ok {
-           where.Exprs = andCondition.Exprs
-        }
-    }
+	if len(where.Exprs) == 1 {
+		if andCondition, ok := where.Exprs[0].(AndConditions); ok {
+			where.Exprs = andCondition.Exprs
+		}
+	}
 
 	// Switch position if the first query expression is a single Or condition
 	for idx, expr := range where.Exprs {
@@ -166,19 +166,58 @@ type NotConditions struct {
 }
 
 func (not NotConditions) Build(builder Builder) {
-	if len(not.Exprs) > 1 {
-		builder.WriteByte('(')
+	anyNegationBuilder := false
+	for _, c := range not.Exprs {
+		if _, ok := c.(NegationExpressionBuilder); ok {
+			anyNegationBuilder = true
+			break
+		}
 	}
 
-	for idx, c := range not.Exprs {
-		if idx > 0 {
-			builder.WriteString(AndWithSpace)
+	if anyNegationBuilder {
+		if len(not.Exprs) > 1 {
+			builder.WriteByte('(')
 		}
 
-		if negationBuilder, ok := c.(NegationExpressionBuilder); ok {
-			negationBuilder.NegationBuild(builder)
-		} else {
-			builder.WriteString("NOT ")
+		for idx, c := range not.Exprs {
+			if idx > 0 {
+				builder.WriteString(AndWithSpace)
+			}
+
+			if negationBuilder, ok := c.(NegationExpressionBuilder); ok {
+				negationBuilder.NegationBuild(builder)
+			} else {
+				builder.WriteString("NOT ")
+				e, wrapInParentheses := c.(Expr)
+				if wrapInParentheses {
+					sql := strings.ToUpper(e.SQL)
+					if wrapInParentheses = strings.Contains(sql, AndWithSpace) || strings.Contains(sql, OrWithSpace); wrapInParentheses {
+						builder.WriteByte('(')
+					}
+				}
+
+				c.Build(builder)
+
+				if wrapInParentheses {
+					builder.WriteByte(')')
+				}
+			}
+		}
+
+		if len(not.Exprs) > 1 {
+			builder.WriteByte(')')
+		}
+	} else {
+		builder.WriteString("NOT ")
+		if len(not.Exprs) > 1 {
+			builder.WriteByte('(')
+		}
+
+		for idx, c := range not.Exprs {
+			if idx > 0 {
+				builder.WriteString(AndWithSpace)
+			}
+
 			e, wrapInParentheses := c.(Expr)
 			if wrapInParentheses {
 				sql := strings.ToUpper(e.SQL)
@@ -193,9 +232,9 @@ func (not NotConditions) Build(builder Builder) {
 				builder.WriteByte(')')
 			}
 		}
-	}
 
-	if len(not.Exprs) > 1 {
-		builder.WriteByte(')')
+		if len(not.Exprs) > 1 {
+			builder.WriteByte(')')
+		}
 	}
 }
diff --git a/scan.go b/scan.go
index 736db4d..415b9f0 100644
--- a/scan.go
+++ b/scan.go
@@ -274,12 +274,16 @@ func Scan(rows Rows, db *DB, mode ScanMode) {
 
 			if !update || reflectValue.Len() == 0 {
 				update = false
-				// if the slice cap is externally initialized, the externally initialized slice is directly used here
-				if reflectValue.Cap() == 0 {
-					db.Statement.ReflectValue.Set(reflect.MakeSlice(reflectValue.Type(), 0, 20))
-				} else if !isArrayKind {
-					reflectValue.SetLen(0)
-					db.Statement.ReflectValue.Set(reflectValue)
+				if isArrayKind {
+					db.Statement.ReflectValue.Set(reflect.Zero(reflectValue.Type()))
+				} else {
+					// if the slice cap is externally initialized, the externally initialized slice is directly used here
+					if reflectValue.Cap() == 0 {
+						db.Statement.ReflectValue.Set(reflect.MakeSlice(reflectValue.Type(), 0, 20))
+					} else {
+						reflectValue.SetLen(0)
+						db.Statement.ReflectValue.Set(reflectValue)
+					}
 				}
 			}
 
diff --git a/schema/constraint.go b/schema/constraint.go
index 5f6beb8..0ed1eab 100644
--- a/schema/constraint.go
+++ b/schema/constraint.go
@@ -8,7 +8,7 @@ import (
 )
 
 // reg match english letters and midline
-var regEnLetterAndMidline = regexp.MustCompile("^[A-Za-z-_]+$")
+var regEnLetterAndMidline = regexp.MustCompile("^[0-9A-Za-z-_]+$")
 
 type CheckConstraint struct {
 	Name       string
diff --git a/schema/field.go b/schema/field.go
index 91e4c0a..ca2e114 100644
--- a/schema/field.go
+++ b/schema/field.go
@@ -664,7 +664,7 @@ func (field *Field) setupValuerAndSetter() {
 				if field.AutoCreateTime == UnixNanosecond || field.AutoUpdateTime == UnixNanosecond {
 					field.ReflectValueOf(ctx, value).SetInt(data.UnixNano())
 				} else if field.AutoCreateTime == UnixMillisecond || field.AutoUpdateTime == UnixMillisecond {
-					field.ReflectValueOf(ctx, value).SetInt(data.UnixNano() / 1e6)
+					field.ReflectValueOf(ctx, value).SetInt(data.UnixMilli())
 				} else {
 					field.ReflectValueOf(ctx, value).SetInt(data.Unix())
 				}
@@ -673,7 +673,7 @@ func (field *Field) setupValuerAndSetter() {
 					if field.AutoCreateTime == UnixNanosecond || field.AutoUpdateTime == UnixNanosecond {
 						field.ReflectValueOf(ctx, value).SetInt(data.UnixNano())
 					} else if field.AutoCreateTime == UnixMillisecond || field.AutoUpdateTime == UnixMillisecond {
-						field.ReflectValueOf(ctx, value).SetInt(data.UnixNano() / 1e6)
+						field.ReflectValueOf(ctx, value).SetInt(data.UnixMilli())
 					} else {
 						field.ReflectValueOf(ctx, value).SetInt(data.Unix())
 					}
@@ -738,7 +738,7 @@ func (field *Field) setupValuerAndSetter() {
 				if field.AutoCreateTime == UnixNanosecond || field.AutoUpdateTime == UnixNanosecond {
 					field.ReflectValueOf(ctx, value).SetUint(uint64(data.UnixNano()))
 				} else if field.AutoCreateTime == UnixMillisecond || field.AutoUpdateTime == UnixMillisecond {
-					field.ReflectValueOf(ctx, value).SetUint(uint64(data.UnixNano() / 1e6))
+					field.ReflectValueOf(ctx, value).SetUint(uint64(data.UnixMilli()))
 				} else {
 					field.ReflectValueOf(ctx, value).SetUint(uint64(data.Unix()))
 				}


Test patch:
diff --git a/callbacks/create_test.go b/callbacks/create_test.go
new file mode 100644
index 0000000..da6b172
--- /dev/null
+++ b/callbacks/create_test.go
@@ -0,0 +1,71 @@
+package callbacks
+
+import (
+	"reflect"
+	"sync"
+	"testing"
+	"time"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+)
+
+var schemaCache = &sync.Map{}
+
+func TestConvertToCreateValues_DestType_Slice(t *testing.T) {
+	type user struct {
+		ID    int `gorm:"primaryKey"`
+		Name  string
+		Email string `gorm:"default:(-)"`
+		Age   int    `gorm:"default:(-)"`
+	}
+
+	s, err := schema.Parse(&user{}, schemaCache, schema.NamingStrategy{})
+	if err != nil {
+		t.Errorf("parse schema error: %v, is not expected", err)
+		return
+	}
+	dest := []*user{
+		{
+			ID:    1,
+			Name:  "alice",
+			Email: "email",
+			Age:   18,
+		},
+		{
+			ID:    2,
+			Name:  "bob",
+			Email: "email",
+			Age:   19,
+		},
+	}
+	stmt := &gorm.Statement{
+		DB: &gorm.DB{
+			Config: &gorm.Config{
+				NowFunc: func() time.Time { return time.Time{} },
+			},
+			Statement: &gorm.Statement{
+				Settings: sync.Map{},
+				Schema:   s,
+			},
+		},
+		ReflectValue: reflect.ValueOf(dest),
+		Dest:         dest,
+	}
+
+	stmt.Schema = s
+
+	values := ConvertToCreateValues(stmt)
+	expected := clause.Values{
+		// column has value + defaultValue column has value (which should have a stable order)
+		Columns: []clause.Column{{Name: "name"}, {Name: "email"}, {Name: "age"}, {Name: "id"}},
+		Values: [][]interface{}{
+			{"alice", "email", 18, 1},
+			{"bob", "email", 19, 2},
+		},
+	}
+	if !reflect.DeepEqual(expected, values) {
+		t.Errorf("expected: %v got %v", expected, values)
+	}
+}
diff --git a/callbacks/helper_test.go b/callbacks/helper_test.go
index 6b76a41..08f94e2 100644
--- a/callbacks/helper_test.go
+++ b/callbacks/helper_test.go
@@ -95,3 +95,63 @@ func TestConvertMapToValuesForCreate(t *testing.T) {
 		})
 	}
 }
+
+func TestConvertSliceOfMapToValuesForCreate(t *testing.T) {
+	testCase := []struct {
+		name   string
+		input  []map[string]interface{}
+		expect clause.Values
+	}{
+		{
+			name: "Test convert slice of string value",
+			input: []map[string]interface{}{
+				{"name": "my name"},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "name"}},
+				Values:  [][]interface{}{{"my name"}},
+			},
+		},
+		{
+			name: "Test convert slice of int value",
+			input: []map[string]interface{}{
+				{"age": 18},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "age"}},
+				Values:  [][]interface{}{{18}},
+			},
+		},
+		{
+			name: "Test convert slice of float value",
+			input: []map[string]interface{}{
+				{"score": 99.5},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "score"}},
+				Values:  [][]interface{}{{99.5}},
+			},
+		},
+		{
+			name: "Test convert slice of bool value",
+			input: []map[string]interface{}{
+				{"active": true},
+			},
+			expect: clause.Values{
+				Columns: []clause.Column{{Name: "active"}},
+				Values:  [][]interface{}{{true}},
+			},
+		},
+	}
+
+	for _, tc := range testCase {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := ConvertSliceOfMapToValuesForCreate(&gorm.Statement{}, tc.input)
+
+			if !reflect.DeepEqual(actual, tc.expect) {
+				t.Errorf("expected %v but got %v", tc.expect, actual)
+			}
+		})
+	}
+
+}
diff --git a/clause/where_test.go b/clause/where_test.go
index aa9d06e..7d5aca1 100644
--- a/clause/where_test.go
+++ b/clause/where_test.go
@@ -105,6 +105,14 @@ func TestWhere(t *testing.T) {
 			"SELECT * FROM `users` WHERE (`users`.`id` <> ? AND NOT `score` <= ?)",
 			[]interface{}{"1", 100},
 		},
+		{
+			[]clause.Interface{clause.Select{}, clause.From{}, clause.Where{
+				Exprs: []clause.Expression{clause.Not(clause.Expr{SQL: "`score` <= ?", Vars: []interface{}{100}},
+					clause.Expr{SQL: "`age` <= ?", Vars: []interface{}{60}})},
+			}},
+			"SELECT * FROM `users` WHERE NOT (`score` <= ? AND `age` <= ?)",
+			[]interface{}{100, 60},
+		},
 	}
 
 	for idx, result := range results {
diff --git a/tests/create_test.go b/tests/create_test.go
index d9b54b7..5e97a54 100644
--- a/tests/create_test.go
+++ b/tests/create_test.go
@@ -582,44 +582,44 @@ func TestCreateWithAutoIncrementCompositeKey(t *testing.T) {
 }
 
 func TestCreateOnConflictWithDefaultNull(t *testing.T) {
-	type OnConfilctUser struct {
+	type OnConflictUser struct {
 		ID     string
 		Name   string `gorm:"default:null"`
 		Email  string
 		Mobile string `gorm:"default:'133xxxx'"`
 	}
 
-	err := DB.Migrator().DropTable(&OnConfilctUser{})
+	err := DB.Migrator().DropTable(&OnConflictUser{})
 	AssertEqual(t, err, nil)
-	err = DB.AutoMigrate(&OnConfilctUser{})
+	err = DB.AutoMigrate(&OnConflictUser{})
 	AssertEqual(t, err, nil)
 
-	u := OnConfilctUser{
-		ID:     "on-confilct-user-id",
-		Name:   "on-confilct-user-name",
-		Email:  "on-confilct-user-email",
-		Mobile: "on-confilct-user-mobile",
+	u := OnConflictUser{
+		ID:     "on-conflict-user-id",
+		Name:   "on-conflict-user-name",
+		Email:  "on-conflict-user-email",
+		Mobile: "on-conflict-user-mobile",
 	}
 	err = DB.Create(&u).Error
 	AssertEqual(t, err, nil)
 
-	u.Name = "on-confilct-user-name-2"
-	u.Email = "on-confilct-user-email-2"
+	u.Name = "on-conflict-user-name-2"
+	u.Email = "on-conflict-user-email-2"
 	u.Mobile = ""
 	err = DB.Clauses(clause.OnConflict{UpdateAll: true}).Create(&u).Error
 	AssertEqual(t, err, nil)
 
-	var u2 OnConfilctUser
+	var u2 OnConflictUser
 	err = DB.Where("id = ?", u.ID).First(&u2).Error
 	AssertEqual(t, err, nil)
-	AssertEqual(t, u2.Name, "on-confilct-user-name-2")
-	AssertEqual(t, u2.Email, "on-confilct-user-email-2")
+	AssertEqual(t, u2.Name, "on-conflict-user-name-2")
+	AssertEqual(t, u2.Email, "on-conflict-user-email-2")
 	AssertEqual(t, u2.Mobile, "133xxxx")
 }
 
 func TestCreateFromMapWithoutPK(t *testing.T) {
 	if !isMysql() {
-		t.Skipf("This test case skipped, because of only supportting for mysql")
+		t.Skipf("This test case skipped, because of only supporting for mysql")
 	}
 
 	// case 1: one record, create from map[string]interface{}
diff --git a/tests/migrate_test.go b/tests/migrate_test.go
index 837d92c..b25b9da 100644
--- a/tests/migrate_test.go
+++ b/tests/migrate_test.go
@@ -1413,10 +1413,10 @@ func TestMigrateSameEmbeddedFieldName(t *testing.T) {
 	err = DB.Table("game_users").AutoMigrate(&GameUser1{})
 	AssertEqual(t, nil, err)
 
-	_, err = findColumnType(&GameUser{}, "stat_ab_ground_destory_count")
+	_, err = findColumnType(&GameUser{}, "stat_ab_ground_destroy_count")
 	AssertEqual(t, nil, err)
 
-	_, err = findColumnType(&GameUser{}, "rate_ground_rb_ground_destory_count")
+	_, err = findColumnType(&GameUser{}, "rate_ground_rb_ground_destroy_count")
 	AssertEqual(t, nil, err)
 }
 
diff --git a/tests/preload_test.go b/tests/preload_test.go
index 26b08d7..14f9413 100644
--- a/tests/preload_test.go
+++ b/tests/preload_test.go
@@ -8,6 +8,8 @@ import (
 	"sync"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 	. "gorm.io/gorm/utils/tests"
@@ -362,6 +364,14 @@ func TestNestedPreloadWithNestedJoin(t *testing.T) {
 		t.Errorf("failed to find value, got err: %v", err)
 	}
 	AssertEqual(t, find2, value)
+
+	var finds []Value
+	err = DB.Joins("Nested.Join").Joins("Nested").Preload("Nested.Preloads").Find(&finds).Error
+	if err != nil {
+		t.Errorf("failed to find value, got err: %v", err)
+	}
+	require.Len(t, finds, 1)
+	AssertEqual(t, finds[0], value)
 }
 
 func TestEmbedPreload(t *testing.T) {
diff --git a/tests/query_test.go b/tests/query_test.go
index cadf716..c0259a1 100644
--- a/tests/query_test.go
+++ b/tests/query_test.go
@@ -554,6 +554,11 @@ func TestNot(t *testing.T) {
 	if !regexp.MustCompile("SELECT \\* FROM .*users.* WHERE .*users.*..*name.* <> .+ AND .*users.*..*age.* <> .+").MatchString(result.Statement.SQL.String()) {
 		t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String())
 	}
+
+	result = dryDB.Not(DB.Where("manager IS NULL").Where("age >= ?", 20)).Find(&User{})
+	if !regexp.MustCompile("SELECT \\* FROM .*users.* WHERE NOT \\(manager IS NULL AND age >= .+\\) AND .users.\\..deleted_at. IS NULL").MatchString(result.Statement.SQL.String()) {
+		t.Fatalf("Build NOT condition, but got %v", result.Statement.SQL.String())
+	}
 }
 
 func TestNotWithAllFields(t *testing.T) {
@@ -1404,3 +1409,22 @@ func TestQueryError(t *testing.T) {
 	}, Value: 1}).Scan(&p2).Error
 	AssertEqual(t, err, gorm.ErrModelValueRequired)
 }
+
+func TestQueryScanToArray(t *testing.T) {
+	err := DB.Create(&User{Name: "testname1", Age: 10}).Error
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	users := [2]*User{{Name: "1"}, {Name: "2"}}
+	err = DB.Model(&User{}).Where("name = ?", "testname1").Find(&users).Error
+	if err != nil {
+		t.Fatal(err)
+	}
+	if users[0] == nil || users[0].Name != "testname1" {
+		t.Error("users[0] not covere")
+	}
+	if users[1] != nil {
+		t.Error("users[1] should be empty")
+	}
+}
diff --git a/tests/table_test.go b/tests/table_test.go
index fa569d3..0d44a15 100644
--- a/tests/table_test.go
+++ b/tests/table_test.go
@@ -2,8 +2,10 @@ package tests_test
 
 import (
 	"regexp"
+	"sync"
 	"testing"
 
+	"gorm.io/driver/postgres"
 	"gorm.io/gorm"
 	"gorm.io/gorm/schema"
 	"gorm.io/gorm/utils/tests"
@@ -172,3 +174,88 @@ func TestTableWithNamer(t *testing.T) {
 		t.Errorf("Table with namer, got %v", sql)
 	}
 }
+
+func TestPostgresTableWithIdentifierLength(t *testing.T) {
+	if DB.Dialector.Name() != "postgres" {
+		return
+	}
+
+	type LongString struct {
+		ThisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongString string `gorm:"unique"`
+	}
+
+	t.Run("default", func(t *testing.T) {
+		db, _ := gorm.Open(postgres.Open(postgresDSN), &gorm.Config{})
+		user, err := schema.Parse(&LongString{}, &sync.Map{}, db.Config.NamingStrategy)
+		if err != nil {
+			t.Fatalf("failed to parse user unique, got error %v", err)
+		}
+
+		constraints := user.ParseUniqueConstraints()
+		if len(constraints) != 1 {
+			t.Fatalf("failed to find unique constraint, got %v", constraints)
+		}
+
+		for key := range constraints {
+			if len(key) != 63 {
+				t.Errorf("failed to find unique constraint, got %v", constraints)
+			}
+		}
+	})
+
+	t.Run("naming strategy", func(t *testing.T) {
+		db, _ := gorm.Open(postgres.Open(postgresDSN), &gorm.Config{
+			NamingStrategy: schema.NamingStrategy{},
+		})
+
+		user, err := schema.Parse(&LongString{}, &sync.Map{}, db.Config.NamingStrategy)
+		if err != nil {
+			t.Fatalf("failed to parse user unique, got error %v", err)
+		}
+
+		constraints := user.ParseUniqueConstraints()
+		if len(constraints) != 1 {
+			t.Fatalf("failed to find unique constraint, got %v", constraints)
+		}
+
+		for key := range constraints {
+			if len(key) != 63 {
+				t.Errorf("failed to find unique constraint, got %v", constraints)
+			}
+		}
+	})
+
+	t.Run("namer", func(t *testing.T) {
+		uname := "custom_unique_name"
+		db, _ := gorm.Open(postgres.Open(postgresDSN), &gorm.Config{
+			NamingStrategy: mockUniqueNamingStrategy{
+				UName: uname,
+			},
+		})
+
+		user, err := schema.Parse(&LongString{}, &sync.Map{}, db.Config.NamingStrategy)
+		if err != nil {
+			t.Fatalf("failed to parse user unique, got error %v", err)
+		}
+
+		constraints := user.ParseUniqueConstraints()
+		if len(constraints) != 1 {
+			t.Fatalf("failed to find unique constraint, got %v", constraints)
+		}
+
+		for key := range constraints {
+			if key != uname {
+				t.Errorf("failed to find unique constraint, got %v", constraints)
+			}
+		}
+	})
+}
+
+type mockUniqueNamingStrategy struct {
+	UName string
+	schema.NamingStrategy
+}
+
+func (a mockUniqueNamingStrategy) UniqueName(table, column string) string {
+	return a.UName
+}