From cda33431fbce956f924eaab38888de1d2abc1660 Mon Sep 17 00:00:00 2001 From: Martin Ottenwaelter Date: Tue, 6 Jun 2023 07:02:36 +0200 Subject: [PATCH] fix: "model does not have column" error This fixes the case of two belongs-to fields with different names but with the same type. --- internal/dbtest/orm_test.go | 155 ++++++++++++++++++++++++++++++++++++ schema/table.go | 37 +++++++-- 2 files changed, 185 insertions(+), 7 deletions(-) diff --git a/internal/dbtest/orm_test.go b/internal/dbtest/orm_test.go index 8edacdc3c..45b76ed4a 100644 --- a/internal/dbtest/orm_test.go +++ b/internal/dbtest/orm_test.go @@ -31,7 +31,9 @@ func TestORM(t *testing.T) { {testRelationExcludeAll}, {testM2MRelationExcludeColumn}, {testRelationBelongsToSelf}, + {testRelationsCycle}, {testCompositeHasMany}, + {testRelationsDifferentFieldsWithSameType}, } testEachDB(t, func(t *testing.T, dbName string, db *bun.DB) { @@ -372,6 +374,65 @@ func testRelationBelongsToSelf(t *testing.T, db *bun.DB) { }, models) } +func testRelationsCycle(t *testing.T, db *bun.DB) { + type Child struct { + Id int64 `bun:",pk"` + SiblingId int64 + Sibling *Child `bun:"rel:belongs-to"` + } + + type Parent struct { + Id int64 `bun:",pk"` + GrandParentId int64 + FirstChildId int64 + FirstChild *Child `bun:"rel:belongs-to"` + SecondChildId int64 + SecondChild *Child `bun:"rel:belongs-to"` + } + + type GrandParent struct { + Id int64 `bun:",pk"` + Parents []*Parent `bun:"rel:has-many"` + } + + err := db.ResetModel(ctx, (*GrandParent)(nil)) + require.NoError(t, err) + err = db.ResetModel(ctx, (*Parent)(nil)) + require.NoError(t, err) + err = db.ResetModel(ctx, (*Child)(nil)) + require.NoError(t, err) + + _, err = db.NewInsert().Model(&GrandParent{Id: 1}).Exec(ctx) + require.NoError(t, err) + _, err = db.NewInsert().Model(&Parent{Id: 1, GrandParentId: 1, FirstChildId: 1, SecondChildId: 2}).Exec(ctx) + require.NoError(t, err) + _, err = db.NewInsert().Model(&Child{Id: 1}).Exec(ctx) + require.NoError(t, err) + _, err = db.NewInsert().Model(&Child{Id: 2, SiblingId: 1}).Exec(ctx) + require.NoError(t, err) + + var grandParent GrandParent + err = db.NewSelect(). + Model(&grandParent). + Relation("Parents.FirstChild"). + Relation("Parents.SecondChild.Sibling"). + Where("grand_parent.id = ?", 1). + Scan(ctx) + + require.NoError(t, err) + require.Equal(t, GrandParent{ + Id: 1, + Parents: []*Parent{{ + Id: 1, + GrandParentId: 1, + FirstChildId: 1, + FirstChild: &Child{Id: 1, SiblingId: 0}, + SecondChildId: 2, + SecondChild: &Child{Id: 2, SiblingId: 1, Sibling: &Child{Id: 1, SiblingId: 0}}, + }}, + }, grandParent) +} + func testM2MRelationExcludeColumn(t *testing.T, db *bun.DB) { type Item struct { ID int64 `bun:",pk,autoincrement"` @@ -442,6 +503,100 @@ func testCompositeHasMany(t *testing.T, db *bun.DB) { require.Equal(t, 2, len(department.Employees)) } +func testRelationsDifferentFieldsWithSameType(t *testing.T, db *bun.DB) { + type Country struct { + Id int `bun:",pk"` + } + + type Address struct { + Id int `bun:",pk"` + CountryId int + Country *Country `bun:"rel:has-one,join:country_id=id"` + } + + type CompanyAddress struct { + Id int `bun:",pk"` + CompanyId int + BillingAddressId int + BillingAddress *Address `bun:"rel:belongs-to,join:billing_address_id=id"` + ShippingAddressId int + ShippingAddress *Address `bun:"rel:belongs-to,join:shipping_address_id=id"` + CountryId int + Country *Country `bun:"rel:has-one,join:country_id=id"` + } + + type Company struct { + Id int `bun:",pk"` + ParentCompanyId int + CompanyAddress *CompanyAddress `bun:"rel:has-one,join:id=company_id"` + } + + type ParentCompany struct { + Id int `bun:",pk"` + Companies []*Company `bun:"rel:has-many,join:id=parent_company_id"` + } + + models := []interface{}{ + (*Country)(nil), + (*Address)(nil), + (*CompanyAddress)(nil), + (*Company)(nil), + (*ParentCompany)(nil), + } + for _, model := range models { + _, err := db.NewDropTable().Model(model).IfExists().Exec(ctx) + require.NoError(t, err) + _, err = db.NewCreateTable().Model(model).Exec(ctx) + require.NoError(t, err) + } + + models = []interface{}{ + &Country{Id: 1}, + &Address{Id: 1, CountryId: 1}, + &CompanyAddress{Id: 1, CompanyId: 1, BillingAddressId: 1, ShippingAddressId: 1, CountryId: 1}, + &Company{Id: 1, ParentCompanyId: 1}, + &ParentCompany{Id: 1}, + } + for _, model := range models { + res, err := db.NewInsert().Model(model).Exec(ctx) + require.NoError(t, err) + + n, err := res.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), n) + } + + var parentCompany ParentCompany + err := db.NewSelect(). + Model(&parentCompany). + Relation("Companies.CompanyAddress.Country"). + Relation("Companies.CompanyAddress.BillingAddress.Country"). + Relation("Companies.CompanyAddress.ShippingAddress.Country"). + Where("parent_company.id = ?", 1). + Scan(ctx) + + require.NoError(t, err) + require.Equal(t, ParentCompany{ + Id: 1, + Companies: []*Company{ + { + Id: 1, + ParentCompanyId: 1, + CompanyAddress: &CompanyAddress{ + Id: 1, + CompanyId: 1, + BillingAddressId: 1, + BillingAddress: &Address{Id: 1, CountryId: 1, Country: &Country{Id: 1}}, + ShippingAddressId: 1, + ShippingAddress: &Address{Id: 1, CountryId: 1, Country: &Country{Id: 1}}, + CountryId: 1, + Country: &Country{Id: 1}, + }, + }, + }, + }, parentCompany) +} + type Genre struct { ID int `bun:",pk"` Name string diff --git a/schema/table.go b/schema/table.go index 9eb7d1bfe..cd0ff20b2 100644 --- a/schema/table.go +++ b/schema/table.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "reflect" + "strconv" "strings" "sync" "time" @@ -806,18 +807,38 @@ func (t *Table) m2mRelation(field *Field) *Relation { return rel } -func (t *Table) inlineFields(field *Field, seen map[reflect.Type]struct{}) { - if seen == nil { - seen = map[reflect.Type]struct{}{t.Type: {}} +type seenKey struct { + Table reflect.Type + FieldIndex string +} + +type seenMap map[seenKey]struct{} + +func NewSeenKey(table reflect.Type, fieldIndex []int) (key seenKey) { + key.Table = table + for _, index := range fieldIndex { + key.FieldIndex += strconv.Itoa(index) + "-" } + return key +} - if _, ok := seen[field.IndirectType]; ok { - return +func (s seenMap) Clone() seenMap { + t := make(seenMap) + for k, v := range s { + t[k] = v + } + return t +} + +func (t *Table) inlineFields(field *Field, seen seenMap) { + if seen == nil { + seen = make(seenMap) } - seen[field.IndirectType] = struct{}{} joinTable := t.dialect.Tables().Ref(field.IndirectType) for _, f := range joinTable.allFields { + key := NewSeenKey(joinTable.Type, f.Index) + f = f.Clone() f.GoName = field.GoName + "_" + f.GoName f.Name = field.Name + "__" + f.Name @@ -834,7 +855,9 @@ func (t *Table) inlineFields(field *Field, seen map[reflect.Type]struct{}) { continue } - if _, ok := seen[f.IndirectType]; !ok { + if _, ok := seen[key]; !ok { + seen = seen.Clone() + seen[key] = struct{}{} t.inlineFields(f, seen) } }