Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions callbacks/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ func BuildQuerySQL(db *gorm.DB) {
}
}

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}
Expand Down Expand Up @@ -158,11 +164,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 := truncatedTableAliases[tableAliasName]; ok {
origTableAliasName = alias
}
db.Statement.ColumnMapping[aliasName] = utils.NestedRelationName(origTableAliasName, s)
}
}

Expand Down Expand Up @@ -232,12 +244,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)
truncatedTableAliases[aliasName] = fullName
curAliasName = aliasName

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

parentTableName = curAliasName
parentFullTableName = fullName
}
} else {
fromClause.Joins = append(fromClause.Joins, clause.Join{
Expand Down
62 changes: 53 additions & 9 deletions schema/naming.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,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"
Expand All @@ -22,6 +24,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
Expand Down Expand Up @@ -95,25 +98,66 @@ func (ns NamingStrategy) UniqueName(table, column string) string {
return ns.formatName("uni", table, ns.toDBName(column))
}

func (ns NamingStrategy) formatName(prefix, table, name string) string {
formattedName := strings.ReplaceAll(strings.Join([]string{
prefix, table, name,
}, "_"), ".", "_")
// 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 utf8.RuneCountInString(formattedName) > ns.IdentifierMaxLength {
h := sha1.New()
if len(formattedName) > ns.IdentifierMaxLength {
h := sha256.New224()
h.Write([]byte(formattedName))
bs := h.Sum(nil)

formattedName = formattedName[0:ns.IdentifierMaxLength-8] + hex.EncodeToString(bs)[:8]
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,
}, "_"), ".", "_")

return ns.truncateName(formattedName)
}

var (
// https://github.com/golang/lint/blob/master/lint.go#L770
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"}
Expand Down
4 changes: 2 additions & 2 deletions schema/naming_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion schema/relationship_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
79 changes: 79 additions & 0 deletions tests/joins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tests_test

import (
"fmt"
"os"
"regexp"
"sort"
"testing"
Expand Down Expand Up @@ -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