bun
bun copied to clipboard
Panic when querying nested relations with the same target model
Happens when nested model has >1 relation with the same target model like this:
type Profile struct {
bun.BaseModel `bun:"profiles"`
ID int64 `bun:"id,pk,autoincrement"`
Tagline string `bun:"tagline"`
ProfileImageID sql.NullInt64 `bun:"profile_image_id"`
ProfileImage *Image `bun:"rel:belongs-to,join:profile_image_id=id"`
CoverImageID sql.NullInt64 `bun:"cover_image_id"`
CoverImage *Image `bun:"rel:belongs-to,join:cover_image_id=id"`
}
Complete example for reproducing:
package main
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"github.com/uptrace/bun/extra/bundebug"
)
type Image struct {
bun.BaseModel `bun:"images"`
ID int64 `bun:"id,pk,autoincrement"`
Path string `bun:"path"`
}
type Profile struct {
bun.BaseModel `bun:"profiles"`
ID int64 `bun:"id,pk,autoincrement"`
Tagline string `bun:"tagline"`
ProfileImageID sql.NullInt64 `bun:"profile_image_id"`
ProfileImage *Image `bun:"rel:belongs-to,join:profile_image_id=id"`
CoverImageID sql.NullInt64 `bun:"cover_image_id"`
CoverImage *Image `bun:"rel:belongs-to,join:cover_image_id=id"`
}
type User struct {
bun.BaseModel `bun:"users,alias:u"`
ID int64 `bun:"id,pk,autoincrement"`
Name string `bun:"name"`
ProfileID int64 `bun:"profile_id"`
Profile *Profile `bun:"rel:belongs-to,join:profile_id=id"`
}
func main() {
ctx := context.Background()
driver := pgdriver.NewConnector(pgdriver.WithDSN("postgres://dealroom:secret@localhost:54321/notifications-test?sslmode=disable"))
db := bun.NewDB(sql.OpenDB(driver), pgdialect.New())
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
db.NewCreateTable().Model((*Image)(nil)).IfNotExists().Exec(ctx)
db.NewCreateTable().Model((*Profile)(nil)).IfNotExists().Exec(ctx)
db.NewCreateTable().Model((*User)(nil)).IfNotExists().Exec(ctx)
p := &Profile{
Tagline: "text",
}
_, err := db.NewInsert().Model(p).Exec(ctx)
if err != nil {
panic(err)
}
u := &User{
Name: "user1",
Profile: p, // profile_id won't be set, need to set id (?!)
}
db.NewInsert().Model(u).Exec(ctx)
if err != nil {
panic(err)
}
u0 := new(User)
err = db.NewSelect().Model(u0).Relation("Profile").Where("u.id = ?", u.ID).Scan(ctx)
if err != nil {
panic(err)
}
fmt.Println(u0.ProfileID) // nil
u.ProfileID = p.ID
_, err = db.NewUpdate().Model(u).Column("profile_id").WherePK().Exec(ctx)
if err != nil {
panic(err)
}
u1 := new(User)
err = db.NewSelect().Model(u1).Relation("Profile").Where("u.id = ?", u.ID).Scan(ctx)
if err != nil {
panic(err)
}
// doesn't panic when select 1st relation
u2 := new(User)
err = db.NewSelect().Model(u2).Relation("Profile").Relation("Profile.ProfileImage").Where("u.id = ?", u.ID).Scan(ctx)
if err != nil {
panic(err)
}
// panics when select 2nd relation
u3 := new(User)
err = db.NewSelect().Model(u3).Relation("Profile").Relation("Profile.CoverImage").Where("u.id = ?", u.ID).Scan(ctx)
if err != nil {
panic(err)
}
// also panics when select both
u4 := new(User)
err = db.NewSelect().Model(u4).Relation("Profile").Relation("Profile.ProfileImage").Relation("Profile.CoverImage").Where("u.id = ?", u.ID).Scan(ctx)
if err != nil {
panic(err)
}
db.NewDropTable().Model((*Image)(nil)).IfExists().Exec(ctx)
db.NewDropTable().Model((*Profile)(nil)).IfExists().Exec(ctx)
db.NewDropTable().Model((*User)(nil)).IfExists().Exec(ctx)
}
Produces:
panic: reflect: call of reflect.Value.Field on ptr Value
goroutine 1 [running]:
reflect.Value.Field({0x1296e00?, 0xc000184650?, 0x101233d?}, 0x12ae100?)
/usr/local/go/src/reflect/value.go:1268 +0xe5
github.com/uptrace/bun/schema.fieldByIndex({0x1296e00?, 0xc000184650?, 0xc0001d2028?}, {0xc0001966d0, 0x2, 0x0?})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/schema/reflect.go:45 +0x5f
github.com/uptrace/bun/schema.(*Field).ScanValue(0xc0001bdb80, {0x1296e00?, 0xc000184650?, 0xf?}, {0x0, 0x0})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/schema/field.go:114 +0x5c
github.com/uptrace/bun.(*structTableModel).scanColumn(0xc0001d49c0, {0xc0001ec184, 0xf}, {0x0, 0x0})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:331 +0x14e
github.com/uptrace/bun.(*structTableModel).ScanColumn(0xc0001d49c0, {0xc0001ec184, 0xf}, {0x0?, 0x0?})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:311 +0x36
github.com/uptrace/bun.(*structTableModel).scanColumn(0xc0001d4900, {0xc0001ec17b, 0x18}, {0x0, 0x0})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:336 +0x375
github.com/uptrace/bun.(*structTableModel).ScanColumn(0xc0001d4900, {0xc0001ec17b, 0x18}, {0x0?, 0x0?})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:311 +0x36
github.com/uptrace/bun.(*structTableModel).Scan(0xc0001ee000?, {0x0?, 0x0?})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:307 +0xb3
database/sql.convertAssignRows({0x12e21c0?, 0xc0001d4900}, {0x0?, 0x0}, 0xc000198300)
/usr/local/go/src/database/sql/convert.go:386 +0x2437
database/sql.(*Rows).Scan(0xc000198300, {0xc0001ac240, 0x9, 0x12a5f60?})
/usr/local/go/src/database/sql/sql.go:3253 +0x365
github.com/uptrace/bun.(*structTableModel).scanRow(0xc0001d4900, {0x1366308, 0xc00009e010}, 0xc0001b9bd8?, {0xc0001ac240, 0x9, 0x9})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:292 +0x73
github.com/uptrace/bun.(*structTableModel).ScanRow(0xc0001d4900, {0x1366308, 0xc00009e010}, 0x242?)
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:283 +0xfd
github.com/uptrace/bun.(*structTableModel).ScanRows(0x0?, {0x1366308, 0xc00009e010}, 0xc0001ee000?)
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:256 +0x51
github.com/uptrace/bun.(*baseQuery).scan(0xc0001e25a0, {0x1366308?, 0xc00009e010?}, {0x1366538?, 0xc0001e25a0?}, {0xc0001ee000, 0x242}, {0x1365a28?, 0xc0001d4900}, 0x1)
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/query_base.go:568 +0x18c
github.com/uptrace/bun.(*SelectQuery).Scan(0xc0001e25a0, {0x1366308, 0xc00009e010}, {0x0?, 0x0?, 0x0?})
/Users/rush/go/pkg/mod/github.com/uptrace/[email protected]/query_select.go:881 +0x17e
main.main()
I've experimented more with that and found a workaround:
If replace Scan with Exec query doesn't fail, it builds correct sql and executes it, sql.Resut -> RowsAffected contains correct number of rows, but data isn't mapped into the struct. So i tried to pass a map into Exec to get raw data and mapping it manually 💁 Quite tricky...
u5 := new(User)
m := make([]map[string]interface{}, 0)
_, err = db.NewSelect().Model(u5).Relation("Profile").Relation("Profile.ProfileImage").Relation("Profile.CoverImage").Where("u.id = ?", u.ID).Exec(ctx, &m)
if err != nil {
panic(err)
}
fmt.Println(m)
Outputs:
[map[id:4 name:user1 profile__cover_image__id:<nil> profile__cover_image__path:<nil> profile__cover_image_id:<nil> profile__id:4 profile__profile_image__id:<nil> profile__profile_image__path:<nil> profile__profile_image_id:<nil> profile__tagline:text profile_id:4]]
But bug is still there
+1 same issue here
@hotrush try to use Image instead of *Image, seems problem solved
@qindj but it must be a pointer because it can be nil, no?
Same error
I faced the same error but with nested data like:
type Segment_A struct {
bun.BaseModel `bun:"Segment_A"`
another_fields....
}
type Segment_B struct {
bun.BaseModel `bun:"Segment_B"`
another_fields....
}
type A struct {
bun.BaseModel `bun:"A"`
ID int64 `bun:"id,pk,autoincrement"`
SegmentAID int64
SegmentA *Segment_A `bun:"rel:has-one,join:segment_a_id=id"`
SegmentBID int64
SegmentB *Segment_B `bun:"rel:has-one,join:segment_b_id=id"`
}
type B struct {
bun.BaseModel `bun:"A"`
ID int64 `bun:"id,pk,autoincrement"`
SegmentAID int64
SegmentA *Segment_A `bun:"rel:has-one,join:segment_a_id=id"`
SegmentBID int64
SegmentB *Segment_B `bun:"rel:has-one,join:segment_b_id=id"`
TypeAID int64
TypeA *A `bun:"rel:has-one,join:type_a_id=id"`
}
In that case with relation to the segments i've got panic: reflect: call of reflect.Value.Field on ptr Value. But when i add into B an A but without segment relations it works fine.
up
seems bun is dead
: (