Fix: Long column identifiers in deeply nested joins cause fields to be omitted #7513

This commit is contained in:
Igor Stepanov 2025-07-17 01:49:31 +03:00
parent 751a6dde7a
commit a4270cc29d
5 changed files with 162 additions and 2 deletions

View File

@ -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{

View File

@ -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 {

View File

@ -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,

View File

@ -47,6 +47,7 @@ type Statement struct {
attrs []interface{}
assigns []interface{}
scopes []func(*DB) *DB
TruncatedAliases map[string]string
Result *result
}

View File

@ -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})
}