Skip to content

Commit 9a48c44

Browse files
committed
Fix: Long column identifiers in deeply nested joins cause fields to be omitted #7513
1 parent 751a6dd commit 9a48c44

File tree

5 files changed

+161
-14
lines changed

5 files changed

+161
-14
lines changed

callbacks/query.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ func BuildQuerySQL(db *gorm.DB) {
4040
}
4141
}
4242

43+
truncatedTableAliases := make(map[string]string)
44+
45+
if db.Statement.ColumnMapping == nil {
46+
db.Statement.ColumnMapping = make(map[string]string)
47+
}
48+
4349
if db.Statement.SQL.Len() == 0 {
4450
db.Statement.SQL.Grow(100)
4551
clauseSelect := clause.Select{Distinct: db.Statement.Distinct}
@@ -158,11 +164,17 @@ func BuildQuerySQL(db *gorm.DB) {
158164
selectColumns, restricted := columnStmt.SelectAndOmitColumns(false, false)
159165
for _, s := range relation.FieldSchema.DBNames {
160166
if v, ok := selectColumns[s]; (ok && v) || (!ok && !restricted) {
167+
aliasName := db.NamingStrategy.JoinNestedRelationNames([]string{tableAliasName, s})
161168
clauseSelect.Columns = append(clauseSelect.Columns, clause.Column{
162169
Table: tableAliasName,
163170
Name: s,
164-
Alias: utils.NestedRelationName(tableAliasName, s),
171+
Alias: aliasName,
165172
})
173+
origTableAliasName := tableAliasName
174+
if alias, ok := truncatedTableAliases[tableAliasName]; ok {
175+
origTableAliasName = alias
176+
}
177+
db.Statement.ColumnMapping[aliasName] = utils.NestedRelationName(origTableAliasName, s)
166178
}
167179
}
168180

@@ -232,12 +244,23 @@ func BuildQuerySQL(db *gorm.DB) {
232244
}
233245

234246
parentTableName := clause.CurrentTable
247+
parentFullTableName := clause.CurrentTable
235248
for idx, rel := range relations {
236249
// joins table alias like "Manager, Company, Manager__Company"
237250
curAliasName := rel.Name
251+
252+
var nameParts []string
253+
var fullName string
238254
if parentTableName != clause.CurrentTable {
239-
curAliasName = utils.NestedRelationName(parentTableName, curAliasName)
255+
nameParts = []string{parentFullTableName, curAliasName}
256+
fullName = utils.NestedRelationName(parentFullTableName, curAliasName)
257+
} else {
258+
nameParts = []string{curAliasName}
259+
fullName = curAliasName
240260
}
261+
aliasName := db.NamingStrategy.JoinNestedRelationNames(nameParts)
262+
truncatedTableAliases[aliasName] = fullName
263+
curAliasName = aliasName
241264

242265
if _, ok := specifiedRelationsName[curAliasName]; !ok {
243266
aliasName := curAliasName
@@ -250,6 +273,7 @@ func BuildQuerySQL(db *gorm.DB) {
250273
}
251274

252275
parentTableName = curAliasName
276+
parentFullTableName = fullName
253277
}
254278
} else {
255279
fromClause.Joins = append(fromClause.Joins, clause.Join{

schema/naming.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package schema
22

33
import (
4-
"crypto/sha1"
4+
"crypto/sha256"
55
"encoding/hex"
66
"regexp"
77
"strings"
88
"unicode/utf8"
99

10+
"gorm.io/gorm/utils"
11+
1012
"github.com/jinzhu/inflection"
1113
"golang.org/x/text/cases"
1214
"golang.org/x/text/language"
@@ -22,6 +24,7 @@ type Namer interface {
2224
CheckerName(table, column string) string
2325
IndexName(table, column string) string
2426
UniqueName(table, column string) string
27+
JoinNestedRelationNames(relationNames []string) string
2528
}
2629

2730
// Replacer replacer interface like strings.Replacer
@@ -95,25 +98,66 @@ func (ns NamingStrategy) UniqueName(table, column string) string {
9598
return ns.formatName("uni", table, ns.toDBName(column))
9699
}
97100

98-
func (ns NamingStrategy) formatName(prefix, table, name string) string {
99-
formattedName := strings.ReplaceAll(strings.Join([]string{
100-
prefix, table, name,
101-
}, "_"), ".", "_")
101+
// JoinNestedRelationNames nested relationships like `Manager__Company` with enforcing IdentifierMaxLength
102+
func (ns NamingStrategy) JoinNestedRelationNames(relationNames []string) string {
103+
tableAlias := utils.JoinNestedRelationNames(relationNames)
104+
return ns.truncateName(tableAlias)
105+
}
102106

107+
// TruncatedName generate truncated name
108+
func (ns NamingStrategy) truncateName(ident string) string {
109+
formattedName := ident
103110
if ns.IdentifierMaxLength == 0 {
104111
ns.IdentifierMaxLength = 64
105112
}
106113

107-
if utf8.RuneCountInString(formattedName) > ns.IdentifierMaxLength {
108-
h := sha1.New()
114+
if len(formattedName) > ns.IdentifierMaxLength {
115+
h := sha256.New224()
109116
h.Write([]byte(formattedName))
110117
bs := h.Sum(nil)
111-
112-
formattedName = formattedName[0:ns.IdentifierMaxLength-8] + hex.EncodeToString(bs)[:8]
118+
formattedName = truncate(formattedName, ns.IdentifierMaxLength-8) + hex.EncodeToString(bs)[:8]
113119
}
114120
return formattedName
115121
}
116122

123+
func truncate(s string, size int) string {
124+
if len(s) <= size {
125+
return s
126+
}
127+
s = s[0:size]
128+
num := brokenTailSize(s)
129+
s = s[0 : len(s)-num]
130+
return s
131+
}
132+
133+
func brokenTailSize(s string) int {
134+
if len(s) == 0 {
135+
return 0
136+
}
137+
res := 1
138+
139+
for i := len(s) - 1; i >= 0; i-- {
140+
char := s[i] & 0b11000000
141+
if char != 0b10000000 {
142+
break
143+
}
144+
res++
145+
}
146+
147+
if utf8.Valid([]byte(s[len(s)-res:])) {
148+
res = 0
149+
}
150+
return res
151+
}
152+
153+
func (ns NamingStrategy) formatName(prefix, table, name string) string {
154+
formattedName := strings.ReplaceAll(strings.Join([]string{
155+
prefix, table, name,
156+
}, "_"), ".", "_")
157+
158+
return ns.truncateName(formattedName)
159+
}
160+
117161
var (
118162
// https://github.com/golang/lint/blob/master/lint.go#L770
119163
commonInitialisms = []string{"API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SSH", "TLS", "TTL", "UID", "UI", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XSRF", "XSS"}

schema/naming_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func TestFormatNameWithStringLongerThan63Characters(t *testing.T) {
193193
ns := NamingStrategy{IdentifierMaxLength: 63}
194194

195195
formattedName := ns.formatName("prefix", "table", "thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongString")
196-
if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVer180f2c67" {
196+
if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVerb463f8ff" {
197197
t.Errorf("invalid formatted name generated, got %v", formattedName)
198198
}
199199
}
@@ -202,7 +202,7 @@ func TestFormatNameWithStringLongerThan64Characters(t *testing.T) {
202202
ns := NamingStrategy{IdentifierMaxLength: 64}
203203

204204
formattedName := ns.formatName("prefix", "table", "thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongString")
205-
if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVery180f2c67" {
205+
if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryb463f8ff" {
206206
t.Errorf("invalid formatted name generated, got %v", formattedName)
207207
}
208208
}

schema/relationship_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ func TestParseConstraintNameWithSchemaQualifiedLongTableName(t *testing.T) {
985985
t.Fatalf("Failed to parse schema")
986986
}
987987

988-
expectedConstraintName := "fk_my_schema_a_very_very_very_very_very_very_very_very_l4db13eec"
988+
expectedConstraintName := "fk_my_schema_a_very_very_very_very_very_very_very_very_l46bfd72a"
989989
constraint := s.Relationships.Relations["Author"].ParseConstraint()
990990

991991
if constraint.Name != expectedConstraintName {

tests/joins_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tests_test
22

33
import (
44
"fmt"
5+
"os"
56
"regexp"
67
"sort"
78
"testing"
@@ -476,3 +477,81 @@ func TestJoinsPreload_Issue7013_NoEntries(t *testing.T) {
476477

477478
AssertEqual(t, len(entries), 0)
478479
}
480+
481+
func TestJoinsLongName_Issue7513(t *testing.T) {
482+
if os.Getenv("GORM_DIALECT") != "postgres" {
483+
// Another DB may not support UTF-8 characters in identifiers
484+
return
485+
}
486+
type (
487+
Owner struct {
488+
gorm.Model
489+
Name string
490+
}
491+
492+
Land struct {
493+
gorm.Model
494+
Address string
495+
OwneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerID uint `gorm:"column:owner_id"`
496+
Owneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer *Owner
497+
}
498+
499+
Design struct {
500+
gorm.Model
501+
Name𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x string
502+
}
503+
504+
Building struct {
505+
gorm.Model
506+
Name string
507+
Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃xID uint `gorm:"column:land_id"`
508+
Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x *Land
509+
510+
DesignID uint `gorm:"column:design_id"`
511+
Design *Design
512+
}
513+
)
514+
515+
DB.Migrator().DropTable(&Building{}, &Owner{}, &Land{}, Design{})
516+
DB.Migrator().AutoMigrate(&Building{}, &Owner{}, &Land{}, Design{})
517+
518+
home := &Building{
519+
Model: gorm.Model{
520+
ID: 1,
521+
},
522+
Name: "Awesome Building",
523+
Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃xID: 2,
524+
Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x: &Land{
525+
Model: gorm.Model{
526+
ID: 2,
527+
},
528+
Address: "Awesome Street",
529+
OwneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerID: 3,
530+
Owneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer: &Owner{
531+
Model: gorm.Model{
532+
ID: 3,
533+
},
534+
Name: "Awesome Person",
535+
},
536+
},
537+
DesignID: 4,
538+
Design: &Design{
539+
Model: gorm.Model{
540+
ID: 4,
541+
},
542+
Name𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x: "Awesome Design",
543+
},
544+
}
545+
DB.Create(home)
546+
547+
var entries []Building
548+
assert.NotPanics(t, func() {
549+
assert.NoError(t,
550+
DB.Joins("Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x").
551+
Joins("Laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaand𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃𒀃x.Owneeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeer").
552+
Joins("Design").
553+
Find(&entries).Error)
554+
})
555+
556+
AssertEqual(t, entries, []Building{*home})
557+
}

0 commit comments

Comments
 (0)