From a4270cc29d51e873241991b090d794a1f76177b9 Mon Sep 17 00:00:00 2001 From: Igor Stepanov Date: Thu, 17 Jul 2025 01:49:31 +0300 Subject: [PATCH 1/3] Fix: Long column identifiers in deeply nested joins cause fields to be omitted #7513 --- callbacks/query.go | 25 ++++++++++++-- scan.go | 3 ++ schema/naming.go | 56 ++++++++++++++++++++++++++++++++ statement.go | 1 + tests/joins_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index 548bf709..9dc8a4cd 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -40,6 +40,9 @@ func BuildQuerySQL(db *gorm.DB) { } } + if db.Statement.TruncatedAliases == nil { + db.Statement.TruncatedAliases = make(map[string]string) + } if db.Statement.SQL.Len() == 0 { db.Statement.SQL.Grow(100) clauseSelect := clause.Select{Distinct: db.Statement.Distinct} @@ -158,11 +161,17 @@ func BuildQuerySQL(db *gorm.DB) { selectColumns, restricted := columnStmt.SelectAndOmitColumns(false, false) for _, s := range relation.FieldSchema.DBNames { if v, ok := selectColumns[s]; (ok && v) || (!ok && !restricted) { + aliasName := db.NamingStrategy.JoinNestedRelationNames([]string{tableAliasName, s}) clauseSelect.Columns = append(clauseSelect.Columns, clause.Column{ Table: tableAliasName, Name: s, - Alias: utils.NestedRelationName(tableAliasName, s), + Alias: aliasName, }) + origTableAliasName := tableAliasName + if alias, ok := db.Statement.TruncatedAliases[tableAliasName]; ok { + origTableAliasName = alias + } + db.Statement.TruncatedAliases[aliasName] = utils.NestedRelationName(origTableAliasName, s) } } @@ -232,12 +241,23 @@ func BuildQuerySQL(db *gorm.DB) { } parentTableName := clause.CurrentTable + parentFullTableName := clause.CurrentTable for idx, rel := range relations { // joins table alias like "Manager, Company, Manager__Company" curAliasName := rel.Name + + var nameParts []string + var fullName string if parentTableName != clause.CurrentTable { - curAliasName = utils.NestedRelationName(parentTableName, curAliasName) + nameParts = []string{parentFullTableName, curAliasName} + fullName = utils.NestedRelationName(parentFullTableName, curAliasName) + } else { + nameParts = []string{curAliasName} + fullName = curAliasName } + aliasName := db.NamingStrategy.JoinNestedRelationNames(nameParts) + db.Statement.TruncatedAliases[aliasName] = fullName + curAliasName = aliasName if _, ok := specifiedRelationsName[curAliasName]; !ok { aliasName := curAliasName @@ -250,6 +270,7 @@ func BuildQuerySQL(db *gorm.DB) { } parentTableName = curAliasName + parentFullTableName = fullName } } else { fromClause.Joins = append(fromClause.Joins, clause.Join{ diff --git a/scan.go b/scan.go index 9a99d024..a67f3e6d 100644 --- a/scan.go +++ b/scan.go @@ -227,6 +227,9 @@ func Scan(rows Rows, db *DB, mode ScanMode) { if sch != nil { matchedFieldCount := make(map[string]int, len(columns)) for idx, column := range columns { + if origName, ok := db.Statement.TruncatedAliases[column]; ok { + column = origName + } if field := sch.LookUpField(column); field != nil && field.Readable { fields[idx] = field if count, ok := matchedFieldCount[column]; ok { diff --git a/schema/naming.go b/schema/naming.go index 6248bde8..155ab358 100644 --- a/schema/naming.go +++ b/schema/naming.go @@ -2,11 +2,14 @@ package schema import ( "crypto/sha1" + "crypto/sha256" "encoding/hex" "regexp" "strings" "unicode/utf8" + "gorm.io/gorm/utils" + "github.com/jinzhu/inflection" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -22,6 +25,7 @@ type Namer interface { CheckerName(table, column string) string IndexName(table, column string) string UniqueName(table, column string) string + JoinNestedRelationNames(relationNames []string) string } // Replacer replacer interface like strings.Replacer @@ -95,6 +99,58 @@ func (ns NamingStrategy) UniqueName(table, column string) string { return ns.formatName("uni", table, ns.toDBName(column)) } +// JoinNestedRelationNames nested relationships like `Manager__Company` with enforcing IdentifierMaxLength +func (ns NamingStrategy) JoinNestedRelationNames(relationNames []string) string { + tableAlias := utils.JoinNestedRelationNames(relationNames) + return ns.truncateName(tableAlias) +} + +// TruncatedName generate truncated name +func (ns NamingStrategy) truncateName(ident string) string { + formattedName := ident + if ns.IdentifierMaxLength == 0 { + ns.IdentifierMaxLength = 64 + } + + if len(formattedName) > ns.IdentifierMaxLength { + h := sha256.New224() + h.Write([]byte(formattedName)) + bs := h.Sum(nil) + formattedName = truncate(formattedName, ns.IdentifierMaxLength-8) + hex.EncodeToString(bs)[:8] + } + return formattedName +} + +func truncate(s string, size int) string { + if len(s) <= size { + return s + } + s = s[0:size] + num := brokenTailSize(s) + s = s[0 : len(s)-num] + return s +} + +func brokenTailSize(s string) int { + if len(s) == 0 { + return 0 + } + res := 1 + + for i := len(s) - 1; i >= 0; i-- { + char := s[i] & 0b11000000 + if char != 0b10000000 { + break + } + res++ + } + + if utf8.Valid([]byte(s[len(s)-res:])) { + res = 0 + } + return res +} + func (ns NamingStrategy) formatName(prefix, table, name string) string { formattedName := strings.ReplaceAll(strings.Join([]string{ prefix, table, name, diff --git a/statement.go b/statement.go index c6183724..ac1562de 100644 --- a/statement.go +++ b/statement.go @@ -47,6 +47,7 @@ type Statement struct { attrs []interface{} assigns []interface{} scopes []func(*DB) *DB + TruncatedAliases map[string]string Result *result } diff --git a/tests/joins_test.go b/tests/joins_test.go index 64a9e407..dcb9c40b 100644 --- a/tests/joins_test.go +++ b/tests/joins_test.go @@ -2,6 +2,7 @@ package tests_test import ( "fmt" + "os" "regexp" "sort" "testing" @@ -476,3 +477,81 @@ func TestJoinsPreload_Issue7013_NoEntries(t *testing.T) { AssertEqual(t, len(entries), 0) } + +func TestJoinsLongName_Issue7513(t *testing.T) { + if os.Getenv("GORM_DIALECT") != "postgres" { + // Another DB may not support UTF-8 characters in identifiers + return + } + type ( + Owner struct { + gorm.Model + Name string + } + + Land struct { + gorm.Model + Address string + OwneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerID uint `gorm:"column:owner_id"` + Owneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer *Owner + } + + Design struct { + gorm.Model + Name𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x string + } + + Building struct { + gorm.Model + Name string + Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃xID uint `gorm:"column:land_id"` + Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x *Land + + DesignID uint `gorm:"column:design_id"` + Design *Design + } + ) + + DB.Migrator().DropTable(&Building{}, &Owner{}, &Land{}, Design{}) + DB.Migrator().AutoMigrate(&Building{}, &Owner{}, &Land{}, Design{}) + + home := &Building{ + Model: gorm.Model{ + ID: 1, + }, + Name: "Awesome Building", + Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃xID: 2, + Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x: &Land{ + Model: gorm.Model{ + ID: 2, + }, + Address: "Awesome Street", + OwneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerID: 3, + Owneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer: &Owner{ + Model: gorm.Model{ + ID: 3, + }, + Name: "Awesome Person", + }, + }, + DesignID: 4, + Design: &Design{ + Model: gorm.Model{ + ID: 4, + }, + Name𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x: "Awesome Design", + }, + } + DB.Create(home) + + var entries []Building + assert.NotPanics(t, func() { + assert.NoError(t, + DB.Joins("Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x"). + Joins("Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x.Owneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer"). + Joins("Design"). + Find(&entries).Error) + }) + + AssertEqual(t, entries, []Building{*home}) +} From 2444fc0d82deaa19cab9c5b9f2674e5f850f283b Mon Sep 17 00:00:00 2001 From: Igor Stepanov Date: Sun, 10 Aug 2025 02:29:33 +0300 Subject: [PATCH 2/3] Using ColumnMapping instead of a new map --- callbacks/query.go | 13 ++++++++----- scan.go | 3 --- schema/naming.go | 14 +------------- statement.go | 1 - 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/callbacks/query.go b/callbacks/query.go index 9dc8a4cd..414c5b31 100644 --- a/callbacks/query.go +++ b/callbacks/query.go @@ -40,9 +40,12 @@ func BuildQuerySQL(db *gorm.DB) { } } - if db.Statement.TruncatedAliases == nil { - db.Statement.TruncatedAliases = make(map[string]string) + truncatedTableAliases := make(map[string]string) + + if db.Statement.ColumnMapping == nil { + db.Statement.ColumnMapping = make(map[string]string) } + if db.Statement.SQL.Len() == 0 { db.Statement.SQL.Grow(100) clauseSelect := clause.Select{Distinct: db.Statement.Distinct} @@ -168,10 +171,10 @@ func BuildQuerySQL(db *gorm.DB) { Alias: aliasName, }) origTableAliasName := tableAliasName - if alias, ok := db.Statement.TruncatedAliases[tableAliasName]; ok { + if alias, ok := truncatedTableAliases[tableAliasName]; ok { origTableAliasName = alias } - db.Statement.TruncatedAliases[aliasName] = utils.NestedRelationName(origTableAliasName, s) + db.Statement.ColumnMapping[aliasName] = utils.NestedRelationName(origTableAliasName, s) } } @@ -256,7 +259,7 @@ func BuildQuerySQL(db *gorm.DB) { fullName = curAliasName } aliasName := db.NamingStrategy.JoinNestedRelationNames(nameParts) - db.Statement.TruncatedAliases[aliasName] = fullName + truncatedTableAliases[aliasName] = fullName curAliasName = aliasName if _, ok := specifiedRelationsName[curAliasName]; !ok { diff --git a/scan.go b/scan.go index a67f3e6d..9a99d024 100644 --- a/scan.go +++ b/scan.go @@ -227,9 +227,6 @@ func Scan(rows Rows, db *DB, mode ScanMode) { if sch != nil { matchedFieldCount := make(map[string]int, len(columns)) for idx, column := range columns { - if origName, ok := db.Statement.TruncatedAliases[column]; ok { - column = origName - } if field := sch.LookUpField(column); field != nil && field.Readable { fields[idx] = field if count, ok := matchedFieldCount[column]; ok { diff --git a/schema/naming.go b/schema/naming.go index 155ab358..12e67101 100644 --- a/schema/naming.go +++ b/schema/naming.go @@ -1,7 +1,6 @@ package schema import ( - "crypto/sha1" "crypto/sha256" "encoding/hex" "regexp" @@ -156,18 +155,7 @@ func (ns NamingStrategy) formatName(prefix, table, name string) string { prefix, table, name, }, "_"), ".", "_") - if ns.IdentifierMaxLength == 0 { - ns.IdentifierMaxLength = 64 - } - - if utf8.RuneCountInString(formattedName) > ns.IdentifierMaxLength { - h := sha1.New() - h.Write([]byte(formattedName)) - bs := h.Sum(nil) - - formattedName = formattedName[0:ns.IdentifierMaxLength-8] + hex.EncodeToString(bs)[:8] - } - return formattedName + return ns.truncateName(formattedName) } var ( diff --git a/statement.go b/statement.go index ac1562de..c6183724 100644 --- a/statement.go +++ b/statement.go @@ -47,7 +47,6 @@ type Statement struct { attrs []interface{} assigns []interface{} scopes []func(*DB) *DB - TruncatedAliases map[string]string Result *result } From 654aecbe295cbdad729fdfa731a2d752f5ce9248 Mon Sep 17 00:00:00 2001 From: Igor Stepanov Date: Sun, 10 Aug 2025 02:34:57 +0300 Subject: [PATCH 3/3] Fix test for another hash function --- schema/naming_test.go | 4 ++-- schema/relationship_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/naming_test.go b/schema/naming_test.go index ab7a5e31..84481e48 100644 --- a/schema/naming_test.go +++ b/schema/naming_test.go @@ -193,7 +193,7 @@ func TestFormatNameWithStringLongerThan63Characters(t *testing.T) { ns := NamingStrategy{IdentifierMaxLength: 63} formattedName := ns.formatName("prefix", "table", "thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongString") - if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVer180f2c67" { + if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVerb463f8ff" { t.Errorf("invalid formatted name generated, got %v", formattedName) } } @@ -202,7 +202,7 @@ func TestFormatNameWithStringLongerThan64Characters(t *testing.T) { ns := NamingStrategy{IdentifierMaxLength: 64} formattedName := ns.formatName("prefix", "table", "thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongString") - if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVery180f2c67" { + if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryb463f8ff" { t.Errorf("invalid formatted name generated, got %v", formattedName) } } diff --git a/schema/relationship_test.go b/schema/relationship_test.go index f1acf2d9..a344cb39 100644 --- a/schema/relationship_test.go +++ b/schema/relationship_test.go @@ -985,7 +985,7 @@ func TestParseConstraintNameWithSchemaQualifiedLongTableName(t *testing.T) { t.Fatalf("Failed to parse schema") } - expectedConstraintName := "fk_my_schema_a_very_very_very_very_very_very_very_very_l4db13eec" + expectedConstraintName := "fk_my_schema_a_very_very_very_very_very_very_very_very_l46bfd72a" constraint := s.Relationships.Relations["Author"].ParseConstraint() if constraint.Name != expectedConstraintName {