bun icon indicating copy to clipboard operation
bun copied to clipboard

Panic when querying nested relations with the same target model

Open hotrush opened this issue 2 years ago • 9 comments

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()

hotrush avatar Jul 25 '23 08:07 hotrush

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

hotrush avatar Jul 25 '23 11:07 hotrush

+1 same issue here

qindj avatar Sep 21 '23 01:09 qindj

@hotrush try to use Image instead of *Image, seems problem solved

qindj avatar Sep 21 '23 01:09 qindj

@qindj but it must be a pointer because it can be nil, no?

hotrush avatar Sep 21 '23 07:09 hotrush

Same error

hsequeda avatar Sep 29 '23 18:09 hsequeda

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.

tsknadaj avatar Oct 02 '23 08:10 tsknadaj

up

yohimik avatar Jan 04 '24 13:01 yohimik

seems bun is dead

hotrush avatar Jan 04 '24 14:01 hotrush

: (

yohimik avatar Jan 04 '24 15:01 yohimik