Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bug] Has many does not support nullable IDs #867

Open
leahrivkin opened this issue Jul 14, 2023 · 2 comments
Open

[bug] Has many does not support nullable IDs #867

leahrivkin opened this issue Jul 14, 2023 · 2 comments

Comments

@leahrivkin
Copy link

leahrivkin commented Jul 14, 2023

If the ID type on a has-many field is not an int64, looking up the slice of related objects does not work.
Replicated with both *int64 and null.Int.

Code snippet (modified from example/rel-has-many/main.go

package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/guregu/null"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/sqlitedialect"
	"github.com/uptrace/bun/driver/sqliteshim"
	"github.com/uptrace/bun/extra/bundebug"
)

type Profile struct {
	ID     int64 `bun:",pk,autoincrement"`
	Lang   string
	Active bool
	UserID null.Int
	Person *User `bun:"rel:belongs-to,join:user_id=id"`
}

// User has many profiles.
type User struct {
	ID       int64 `bun:",pk,autoincrement"`
	Name     string
	Profiles []*Profile `bun:"rel:has-many,join:id=user_id"`
}

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

	sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
	if err != nil {
		panic(err)
	}

	db := bun.NewDB(sqldb, sqlitedialect.New())
	defer db.Close()

	db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))

	if err := createSchema(ctx, db); err != nil {
		panic(err)
	}

	user := new(User)
	err = db.NewSelect().
		Model(user).
		Column("user.*").
		Relation("Profiles", func(q *bun.SelectQuery) *bun.SelectQuery {
			return q.Where("active IS TRUE")
		}).
		OrderExpr("user.id ASC").
		Limit(1).
		Scan(ctx)

	if err != nil {
		fmt.Printf("error getting user: %v\n", err)
		// panic(err)
	} else {
		fmt.Printf("got user: %#v\n", user)
	}

	profile := new(Profile)
	if err := db.NewSelect().
		Model(profile).
		Relation("Person").
		Limit(1).
		Scan(ctx); err != nil {

		fmt.Printf("error getting profile: %v\n", err)
		// panic(err)
	} else {
		fmt.Printf("got profile: %d %s %t %v\n", profile.ID, profile.Lang, profile.Active, profile.Person)
	}
}

func createSchema(ctx context.Context, db *bun.DB) error {
	models := []interface{}{
		(*User)(nil),
		(*Profile)(nil),
	}
	for _, model := range models {
		if _, err := db.NewCreateTable().Model(model).Exec(ctx); err != nil {
			return err
		}
	}

	users := []*User{
		{ID: 1, Name: "user 1"},
		{ID: 2, Name: "user 2"},
	}
	if _, err := db.NewInsert().Model(&users).Exec(ctx); err != nil {
		return err
	}

	profiles := []*Profile{
		{ID: 1, Lang: "en", Active: true, UserID: null.IntFrom(1)},
		{ID: 2, Lang: "ru", Active: true, UserID: null.IntFrom(1)},
		{ID: 3, Lang: "md", Active: false, UserID: null.IntFrom(1)},
	}
	if _, err := db.NewInsert().Model(&profiles).Exec(ctx); err != nil {
		return err
	}

	return nil
}

Output (truncated)

[bun]  17:03:28.936   SELECT                  126µs  SELECT "profile"."id", "profile"."lang", "profile"."active", "profile"."user_id" FROM "profiles" AS "profile" WHERE ("profile"."user_id" IN (1)) AND (active IS TRUE)
error getting user: bun: has-many relation=Profiles does not have base model=User with id=[{{'\x01' %!q(bool=true)}}] (check join conditions)

[bun]  17:03:28.936   SELECT                   26µs  SELECT "profile"."id", "profile"."lang", "profile"."active", "profile"."user_id", "person"."id" AS "person__id", "person"."name" AS "person__name" FROM "profiles" AS "profile" LEFT JOIN "users" AS "person" ON ("person"."id" = "profile"."user_id") LIMIT 1
got profile: 1 en true &{1 user 1 []}

Stacktrace

github.com/alexlast/bunzap.QueryHook.AfterQuery
        pkg/mod/github.com/alexlast/bunzap@v0.1.0/bunzap.go:60
github.com/uptrace/bun.(*DB).afterQueryFromIndex
        src/github.com/uptrace/bun/hook.go:114
github.com/uptrace/bun.(*DB).afterQuery
        src/github.com/uptrace/bun/hook.go:109
github.com/uptrace/bun.(*baseQuery).scan
        src/github.com/uptrace/bun/query_base.go:570
github.com/uptrace/bun.(*SelectQuery).Scan
        src/github.com/uptrace/bun/query_select.go:881
github.com/uptrace/bun.(*relationJoin).selectMany
        src/github.com/uptrace/bun/relation_join.go:53
github.com/uptrace/bun.(*SelectQuery).selectJoins
        src/github.com/uptrace/bun/query_select.go:473
github.com/uptrace/bun.(*SelectQuery).Scan
        src/github.com/uptrace/bun/query_select.go:888
main.main
        src/github.com/uptrace/bun/example/rel-has-many/main.go:68
runtime.main
        /usr/local/go/src/runtime/proc.go:250
@omerfruk
Copy link

Hello,

I encountered the same issue and went a step further to identify the root cause. However, this may not necessarily be a problem; it could be a matter of preference. For instance, when establishing a BelongsTo relations, you can set the value as null, but doing the same for a HasMany relations results in an error. This behavior might be intentional. Therefore, I wanted to ask you whether this is considered an error or a matter of preference. I have provided screenshots of the relevant field below.

Thank you.

Screenshot 2023-07-14 at 18 13 02

As seen above, if the value is a pointer(which can be null), it throws an error.

Screenshot 2023-07-14 at 19 14 53

However, as seen here, if the data is not a pointer(cannot be null), you do not receive an error, just like in the documentation.

bun/model_table_has_many.go

Lines 105 to 130 in 838a3bc

func (m *hasManyModel) parkStruct() error {
baseValues, ok := m.baseValues[internal.NewMapKey(m.structKey)]
if !ok {
return fmt.Errorf(
"bun: has-many relation=%s does not have base %s with id=%q (check join conditions)",
m.rel.Field.GoName, m.baseTable, m.structKey)
}
for i, v := range baseValues {
if !m.sliceOfPtr {
v.Set(reflect.Append(v, m.strct))
continue
}
if i == 0 {
v.Set(reflect.Append(v, m.strct.Addr()))
continue
}
clone := reflect.New(m.strct.Type()).Elem()
clone.Set(m.strct)
v.Set(reflect.Append(v, clone.Addr()))
}
return nil
}

The location where the error is being

If this is indeed a bug, I would be glad to assist you in troubleshooting and resolving the issue.

@leahrivkin
Copy link
Author

Thank you @omerfruk ! The main source of confusion was that null.Int works perfectly for belongs-to. Our workaround is for now is to just not use has-many

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants