bun icon indicating copy to clipboard operation
bun copied to clipboard

m2m relation with extend panic

Open gwendalF opened this issue 6 months ago • 1 comments

Bun version: v1.2.11 Driver: pgdriver v1.2.11

I have an issue writing a query with a m2m relation when the member is a struct with the bun:",extend" struct tag:

type Wrapper struct { Order bun:",extend" Number string } type Order struct { bun.BaseModel Id int bun:",pk,autoincrement" Items []Item bun:"m2m:order_to_items,join:Order=Item" }

type Item struct { bun.BaseModel Id int bun:",pk,autoincrement" Name string }

type OrderToItem struct { bun.BaseModel OrderId int Order *Order bun:"rel:belongs-to,join:order_id=id" ItemId int Item *Item bun:"rel:belongs-to,join:item_id=id" }

order := model.Wrapper{
	Number: "Invoice-1",
}
_, err := d.NewInsert().
	Model(&order).
	Exec(t.Context())
assert.NoError(err)
items := []model.Item{
	{Name: "itemA"},
	{Name: "itemB"},
}
_, err = d.NewInsert().
	Model(&items).
	Exec(t.Context())
assert.NoError(err)
_, err = d.NewInsert().
	Model(&[]model.OrderToItem{
		{
			OrderId: order.Id,
			ItemId:  items[0].Id,
		},
		{
			OrderId: order.Id,
			ItemId:  items[1].Id,
		},
	}).
	Exec(t.Context())
assert.NoError(err)

var saved model.Wrapper
err = d.NewSelect().
	Model(&saved).
	Relation("Items").
	Scan(t.Context())
assert.NoError(err)

There is a panic panic: reflect: call of reflect.Value.Int on string Value It works if I use the Order type directly instead of Wrapper Here the stacktrace:

panic: reflect: call of reflect.Value.Int on string Value [recovered]
	panic: reflect: call of reflect.Value.Int on string Value

goroutine 8 [running]:
testing.tRunner.func1.2({0xa81040, 0xc00039c648})
	/usr/local/go/src/testing/testing.go:1734 +0x21c
testing.tRunner.func1()
	/usr/local/go/src/testing/testing.go:1737 +0x35e
panic({0xa81040?, 0xc00039c648?})
	/usr/local/go/src/runtime/panic.go:792 +0x132
reflect.Value.Int(...)
	/usr/local/go/src/reflect/value.go:1465
github.com/uptrace/bun/schema.isZeroInt({0xa640a0?, 0xc0005040e0?, 0x4043b9?})
	app/vendor/github.com/uptrace/bun/schema/zerochecker.go:138 +0xa5
github.com/uptrace/bun/schema.(*Field).AppendValue(0xc00031a780, {{0x12ee4b8?, 0xc00039cd50?}, 0x0?}, {0xc0001c87e0, 0x4d, 0x60}, {0xacfd60, 0xc0005040c0, 0x199})
	app/vendor/github.com/uptrace/bun/schema/field.go:107 +0x14e
github.com/uptrace/bun.appendChildValues.func1({0xacfd60?, 0xc0005040c0?, 0x5?})
	app/vendor/github.com/uptrace/bun/relation_join.go:372 +0x2c5
github.com/uptrace/bun.visitField({0xacfd60?, 0xc0005040c0?, 0x0?}, {0xc0005031c0, 0x0, 0x2}, 0xc0000ef948)
	app/vendor/github.com/uptrace/bun/util.go:43 +0xcf
github.com/uptrace/bun.walk({0xacfd60?, 0xc0005040c0?, 0x7fe33c1ca7f0?}, {0xc0005031c0, 0x0, 0x2}, 0xc0000ef948)
	app/vendor/github.com/uptrace/bun/util.go:30 +0x9f
github.com/uptrace/bun.appendChildValues({{0x12ee4b8?, 0xc00039cd50?}, 0x0?}, {0xc0001c87e0, 0x4d, 0x60}, {0xacfd60?, 0xc0005040c0?, 0x3f?}, {0xc0005031c0, ...}, ...)
	app/vendor/github.com/uptrace/bun/relation_join.go:362 +0x1d1
github.com/uptrace/bun.(*relationJoin).m2mQuery(0xc00020e0e0, 0xc0003005a0)
	app/vendor/github.com/uptrace/bun/relation_join.go:216 +0xa28
github.com/uptrace/bun.(*relationJoin).selectM2M(0x47243a?, {0x12e9500, 0xc000276eb0}, 0x12e6038?)
	app/vendor/github.com/uptrace/bun/relation_join.go:172 +0x25
github.com/uptrace/bun.(*SelectQuery).selectJoins(0xc0003003c0, {0x12e9500, 0xc000276eb0}, {0xc00020e0e0, 0x1, 0xc0003003c0?})
	app/vendor/github.com/uptrace/bun/query_select.go:484 +0x254
github.com/uptrace/bun.(*SelectQuery).scanResult(0xc0003003c0, {0x12e9500, 0xc000276eb0}, {0x0?, 0x0, 0x0?})
	vendor/github.com/uptrace/bun/query_select.go:898 +0x36b
github.com/uptrace/bun.(*SelectQuery).Scan(...)
	vendor/github.com/uptrace/bun/query_select.go:849

gwendalF avatar Jul 10 '25 15:07 gwendalF

I’m sorry — using join together with extend seems to have some bugs. Fixing this issue looks like it will take some time.

Reproducible code.

import (
	"context"
	"database/sql"

	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/pgdialect"
	"github.com/uptrace/bun/driver/pgdriver"
	"github.com/uptrace/bun/extra/bundebug"
)

type Wrapper struct {
	Order  `bun:",extend"`
	Number string
}
type Order struct {
	bun.BaseModel
	Id    int    `bun:",pk,autoincrement"`
	Items []Item `bun:"m2m:order_to_items,join:Order=Item"`
}

type Item struct {
	bun.BaseModel
	Id   int `bun:",pk,autoincrement"`
	Name string
}

type OrderToItem struct {
	bun.BaseModel
	OrderId int
	Order   *Order `bun:"rel:belongs-to,join:order_id=id"`
	ItemId  int
	Item    *Item `bun:"rel:belongs-to,join:item_id=id"`
}

func main() {
	ctx := context.Background()

	dsn := "postgres://postgres:@localhost:5432/postgres?sslmode=disable"
	sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))

	db := bun.NewDB(sqldb, pgdialect.New())
	db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))

	if err := db.ResetModel(ctx, (*Item)(nil), (*OrderToItem)(nil), (*Wrapper)(nil)); err != nil {
		panic(err)
	}

	order := Wrapper{
		Number: "Invoice-1",
	}
	_, err := db.NewInsert().
		Model(&order).
		Exec(ctx)
	if err != nil {
		panic(err)
	}
	items := []Item{
		{Name: "itemA"},
		{Name: "itemB"},
	}
	_, err = db.NewInsert().
		Model(&items).
		Exec(ctx)
	if err != nil {
		panic(err)
	}
	_, err = db.NewInsert().
		Model(&[]OrderToItem{
			{
				OrderId: order.Id,
				ItemId:  items[0].Id,
			},
			{
				OrderId: order.Id,
				ItemId:  items[1].Id,
			},
		}).
		Exec(ctx)
	if err != nil {
		panic(err)
	}

	var saved Wrapper
	err = db.NewSelect().
		Model(&saved).
		Relation("Items").
		Scan(ctx)
	if err != nil {
		panic(err)
	}
}

j2gg0s avatar Jul 11 '25 05:07 j2gg0s