Fix: Long column identifiers in deeply nested joins cause fields to be omitted #7513
This commit is contained in:
		
							parent
							
								
									751a6dde7a
								
							
						
					
					
						commit
						a4270cc29d
					
				| @ -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{ | ||||
|  | ||||
							
								
								
									
										3
									
								
								scan.go
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								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 { | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -47,6 +47,7 @@ type Statement struct { | ||||
| 	attrs                []interface{} | ||||
| 	assigns              []interface{} | ||||
| 	scopes               []func(*DB) *DB | ||||
| 	TruncatedAliases     map[string]string | ||||
| 	Result               *result | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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}) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Igor Stepanov
						Igor Stepanov