-
-
Notifications
You must be signed in to change notification settings - Fork 232
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
m2m relation detection incorrect (v1.2.7) #1100
Comments
Which dialect are you using? Is the usage like this? It works fine locally for me.
[bun] 09:22:57.921 DROP TABLE 15.314ms DROP TABLE IF EXISTS "user_roles" CASCADE
[bun] 09:22:57.930 CREATE TABLE 8.358ms CREATE TABLE "user_roles" ("user_id" VARCHAR NOT NULL, "role_id" VARCHAR NOT NULL, PRIMARY KEY ("user_id", "role_id"))
[bun] 09:22:57.930 DROP TABLE 240µs DROP TABLE IF EXISTS "users" CASCADE
[bun] 09:22:57.934 CREATE TABLE 3.816ms CREATE TABLE "users" ("id" VARCHAR(100) NOT NULL, "status" VARCHAR(100) NOT NULL DEFAULT 'Active', "name" VARCHAR(255) NOT NULL, "username" VARCHAR(20) NOT NULL, PRIMARY KEY ("id"))
[bun] 09:22:57.934 DROP TABLE 251µs DROP TABLE IF EXISTS "roles" CASCADE
[bun] 09:22:57.937 CREATE TABLE 2.509ms CREATE TABLE "roles" ("id" VARCHAR(100) NOT NULL, "name" VARCHAR(100) NOT NULL, "description" TEXT, "parent_role_id" VARCHAR(100), PRIMARY KEY ("id"))
[bun] 09:22:57.937 DROP TABLE 109µs DROP TABLE IF EXISTS "role_permissions" CASCADE
[bun] 09:22:57.939 CREATE TABLE 1.656ms CREATE TABLE "role_permissions" ("role_id" VARCHAR(100) NOT NULL, "permission_id" VARCHAR(100) NOT NULL, PRIMARY KEY ("role_id", "permission_id"))
[bun] 09:22:57.939 DROP TABLE 103µs DROP TABLE IF EXISTS "permissions" CASCADE
[bun] 09:22:57.940 CREATE TABLE 1.356ms CREATE TABLE "permissions" ("id" VARCHAR(100) NOT NULL, PRIMARY KEY ("id"))
[bun] 09:22:57.941 INSERT 936µs INSERT INTO "users" ("id", "status", "name", "username") VALUES ('ua', DEFAULT, '', '') RETURNING "status"
[bun] 09:22:57.942 INSERT 631µs INSERT INTO "roles" ("id", "name", "description", "parent_role_id") VALUES ('ra', '', '', DEFAULT) RETURNING "parent_role_id"
[bun] 09:22:57.943 INSERT 772µs INSERT INTO "user_roles" ("user_id", "role_id") VALUES ('ua', 'ra')
[bun] 09:22:57.944 SELECT 1.523ms SELECT "r"."id", "r"."name", "r"."description", "r"."parent_role_id" FROM "roles" AS "r" JOIN user_roles ur ON ur.role_id = r.id WHERE (ur.user_id = 'ua')
1 |
Apologies for not including that I use postgresql in the original response. Apologies again as I was mistaken before on what was actually causing the error. It appears this is the culprit, not what I reported originally. func (ur *userRepository) GetByID(ctx context.Context, userID pulid.ID, opts repositories.GetByIDOptions) (*user.User, error) {
dba, err := ur.db.DB(ctx)
if err != nil {
return nil, eris.Wrap(err, "get database connection")
}
u := new(user.User)
q := dba.NewSelect().Model(u).Where("usr.id = ?", userID)
// Include roles and permissions if needed
if opts.IncludeRoles {
q.Relation("Roles").Relation("Roles.Permissions")
}
// Include organizations if needed
if opts.IncludeOrgs {
q.Relation("Organizations")
}
if err = q.Scan(ctx); err != nil {
return nil, eris.Wrapf(err, "failed to get user by id %s", userID)
}
return u, nil
} Try adding just Additionally, if I take that out and just leave
edit: Here is how I use the database connection. // Connection is a wrapper around the bun.DB type that provides a way to get a database connection
// and close the connection.
type Connection interface {
// DB returns a database connection.
DB(ctx context.Context) (*bun.DB, error)
// Close closes the database connection.
Close() error
}
type ConnectionParams struct {
fx.In
Config *config.Manager
Logger *logger.Logger
LC fx.Lifecycle
}
var (
DBConnStringEmpty = eris.New("database connection string is empty")
DBConfigNil = eris.New("database config is nil")
AppConfigNil = eris.New("application config is nil")
)
type connection struct {
cfg *config.Manager
log *zerolog.Logger
db *bun.DB
pool *pgxpool.Pool
mu sync.RWMutex
}
func NewConnection(p ConnectionParams) db.Connection {
log := p.Logger.With().
Str("component", "postgres").
Str("service", "connection").
Logger()
conn := &connection{
cfg: p.Config,
log: &log,
}
p.LC.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
_, err := conn.DB(ctx)
if err != nil {
return err
}
return nil
},
OnStop: func(context.Context) error {
return conn.Close()
},
})
return conn
}
func (c *connection) DB(ctx context.Context) (*bun.DB, error) {
c.mu.RLock()
if c.db != nil {
defer c.mu.RUnlock()
return c.db, nil
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
// Double-check after acquiring write lock
if c.db != nil {
return c.db, nil
}
dsn := c.cfg.GetDSN()
if dsn == "" {
return nil, DBConnStringEmpty
}
appCfg := c.cfg.App()
if appCfg == nil {
return nil, AppConfigNil
}
dbCfg := c.cfg.Database()
if dbCfg == nil {
return nil, DBConfigNil
}
// Parse the database connection string
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, eris.Wrap(err, "failed to parse database config")
}
cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
// Setup connection pool
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, eris.Wrap(err, "failed to create database pool")
}
c.pool = pool
sqldb := stdlib.OpenDBFromPool(pool)
bunDB := bun.NewDB(sqldb, pgdialect.New(), bun.WithDiscardUnknownColumns())
// If the environment is development and debug is enabled, add a query hook to the database
if appCfg.Environment == "development" && dbCfg.Debug {
bunDB.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"),
))
}
bunDB.RegisterModel(registry.RegisterEntities()...)
// Configure connection pool settings
sqldb.SetConnMaxIdleTime(time.Duration(dbCfg.ConnMaxIdleTime) * time.Second)
sqldb.SetMaxOpenConns(dbCfg.MaxConnections)
sqldb.SetMaxIdleConns(dbCfg.MaxIdleConns)
sqldb.SetConnMaxLifetime(time.Duration(dbCfg.ConnMaxLifetime) * time.Second)
// Verify connection
if err = bunDB.PingContext(ctx); err != nil {
return nil, eris.Wrap(err, "failed to ping database")
}
c.db = bunDB
c.log.Info().Msg("🚀 Established connection to Postgres database!")
return c.db, nil
}
func (c *connection) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
// Close the connection pool
if c.pool != nil {
c.pool.Close()
}
// Close the database connection
if c.db != nil {
if err := c.db.Close(); err != nil {
return eris.Wrap(err, "failed to close database connection")
}
}
return nil
} |
Don’t mind these. I replaced the driver with pgx, but the following code is still correct.
|
Found the issue, it's caused by how I generate ID's. I assume this change causes this to no longer work in v1.2.7? cb8c42c // pulid/pulid.go
package pulid
import (
"crypto/rand"
"database/sql/driver"
"fmt"
"time"
"github.com/oklog/ulid/v2"
"github.com/rotisserie/eris"
)
var (
ErrScanningNil = eris.New("pulid: scanning nil into PULID")
ErrInvalidLength = eris.New("pulid: invalid length")
)
// ID implements a PULID - a prefixed ULID.
type ID string
func (u ID) IsNil() bool { return u == "" }
func (u ID) IsNotNil() bool { return !u.IsNil() }
var Nil = ID("")
// The default entropy source.
var defaultEntropySource *ulid.MonotonicEntropy
func init() {
// Seed the default entropy source.
defaultEntropySource = ulid.Monotonic(rand.Reader, 0)
}
// newULID returns a new ULID for time.Now() using the default entropy source.
func newULID() ulid.ULID {
return ulid.MustNew(ulid.Timestamp(time.Now()), defaultEntropySource)
}
// MustNew returns a new PULID for time.Now() given a prefix. This uses the default entropy source.
func MustNew(prefix string) ID { return ID(prefix + newULID().String()) }
// Must return a
func Must(prefix string) *ID {
id := MustNew(prefix)
return &id
}
// Scan implements the Scanner interface.
func (u *ID) Scan(src any) error {
if src == nil {
return ErrScanningNil
}
switch v := src.(type) {
case string:
*u = ID(v)
case ID:
*u = v
default:
return fmt.Errorf("pulid: unexpected type, %T", v)
}
return nil
}
// Value implements the driver Valuer interface.
func (u ID) Value() (driver.Value, error) {
return string(u), nil
}
// String returns the string representation of the PULID.
func (u ID) String() string { return string(u) }
// Parse parses a PULID from a string.
func Parse(s string) (ID, error) {
if len(s) < 27 {
return Nil, ErrInvalidLength
}
return ID(s), nil
}
// MustParse parses a PULID from a string. If the string is not a valid PULID, it panics.
func MustParse(s string) (ID, error) {
id, err := Parse(s)
if err != nil {
return Nil, eris.Wrap(err, "pulid: failed to parse PULID")
}
return id, nil
}
//main.go
package main
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/trenova-test/pulid"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/extra/bundebug"
)
type BusinessUnit struct {
bun.BaseModel `bun:"table:business_units,alias:bu" json:"-"`
ID pulid.ID `json:"id" bun:",pk,type:VARCHAR(100)"`
ParentBusinessUnitID *pulid.ID `json:"parentBusinessUnitId" bun:"parent_business_unit_id,type:VARCHAR(100),nullzero"`
Name string `json:"name" bun:"name,type:VARCHAR(100),notnull"`
ParentBusinessUnit *BusinessUnit `json:"parentBusinessUnit" bun:"rel:belongs-to,join:parent_business_unit_id=id"`
}
type Organization struct {
bun.BaseModel `bun:"table:organizations,alias:org" json:"-"`
ID pulid.ID `json:"id" bun:",pk,type:VARCHAR(100)"`
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,type:VARCHAR(100),notnull"`
StateID pulid.ID `json:"stateId" bun:"state_id,type:VARCHAR(100),notnull"`
Name string `json:"name" bun:"name,type:VARCHAR(100),notnull"`
BusinessUnit *BusinessUnit `json:"businessUnit,omitempty" bun:"rel:belongs-to,join:business_unit_id=id"`
}
// models.go
type User struct {
bun.BaseModel `bun:"table:users,alias:usr" json:"-"`
ID pulid.ID `json:"id" bun:"id,pk,type:VARCHAR(100)"`
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,type:VARCHAR(100),notnull"`
CurrentOrganizationID pulid.ID `json:"currentOrganizationId" bun:"current_organization_id,type:VARCHAR(100),notnull"`
Status string `json:"status" bun:"status,type:VARCHAR(100),notnull,default:'Active'"`
Name string `json:"name" bun:"name,type:VARCHAR(255),notnull"`
Username string `json:"username" bun:"username,type:VARCHAR(20),notnull"`
BusinessUnit *BusinessUnit `json:"-" bun:"rel:belongs-to,join:business_unit_id=id"`
CurrentOrganization *Organization `json:"currentOrganization,omitempty" bun:"rel:belongs-to,join:current_organization_id=id"`
Organizations []*Organization `json:"organizations,omitempty" bun:"m2m:user_organizations,join:User=Organization"`
Roles []*Role `json:"roles,omitempty" bun:"m2m:user_roles,join:User=Role"`
}
type UserRole struct {
bun.BaseModel `bun:"table:user_roles,alias:ur" json:"-"`
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,pk,type:VARCHAR(100),notnull"`
OrganizationID pulid.ID `json:"organizationId" bun:"organization_id,pk,type:VARCHAR(100),notnull"`
UserID pulid.ID `json:"userId" bun:"user_id,pk,type:VARCHAR(100),notnull"`
RoleID pulid.ID `json:"roleId" bun:"role_id,pk,type:VARCHAR(100),notnull"`
User *User `json:"-" bun:"rel:belongs-to,join:user_id=id"`
Role *Role `json:"-" bun:"rel:belongs-to,join:role_id=id"`
}
type Role struct {
bun.BaseModel `bun:"table:roles,alias:r" json:"-"`
ID pulid.ID `json:"id" bun:",pk,type:VARCHAR(100)"`
Name string `json:"name" bun:"name,type:VARCHAR(100),notnull"`
Description string `json:"description" bun:"description,type:TEXT"`
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,type:VARCHAR(100)"`
OrganizationID pulid.ID `json:"organizationId" bun:"organization_id,type:VARCHAR(100)"`
ParentRoleID *pulid.ID `json:"parentRoleId,omitempty" bun:"parent_role_id,type:VARCHAR(100),nullzero"`
Permissions []*Permission `json:"permissions,omitempty" bun:"m2m:role_permissions,join:Role=Permission"`
ParentRole *Role `json:"parentRole,omitempty" bun:"rel:belongs-to,join:parent_role_id=id"`
ChildRoles []*Role `json:"childRoles,omitempty" bun:"rel:has-many,join:id=parent_role_id"`
}
type RolePermission struct {
bun.BaseModel `bun:"table:role_permissions,alias:rp" json:"-"`
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,pk,type:VARCHAR(100),notnull"`
OrganizationID pulid.ID `json:"organizationId" bun:"organization_id,pk,type:VARCHAR(100),notnull"`
RoleID pulid.ID `json:"roleId" bun:"role_id,pk,type:VARCHAR(100),notnull"`
PermissionID pulid.ID `json:"permissionId" bun:"permission_id,pk,type:VARCHAR(100),notnull"`
Role *Role `json:"-" bun:"rel:belongs-to,join:role_id=id"`
Permission *Permission `json:"-" bun:"rel:belongs-to,join:permission_id=id"`
}
type Permission struct {
bun.BaseModel `bun:"table:permissions,alias:p" json:"-"`
ID pulid.ID `bun:",pk,type:VARCHAR(100)"`
}
type UserOrganization struct {
bun.BaseModel `bun:"table:user_organizations,alias:uo" json:"-"`
UserID pulid.ID `json:"userId" bun:",pk,type:VARCHAR(100)"`
OrganizationID pulid.ID `json:"organizationId" bun:",pk,type:VARCHAR(100)"`
CreatedAt int64 `json:"createdAt" bun:"created_at,type:BIGINT,nullzero,notnull,default:extract(epoch from current_timestamp)::bigint"`
User *User `json:"user,omitempty" bun:"rel:belongs-to,join:user_id=id"`
Organization *Organization `json:"organization,omitempty" bun:"rel:belongs-to,join:organization_id=id"`
}
func RegisterEntities() []any {
return []any{
&BusinessUnit{},
&Organization{},
&RolePermission{},
&Permission{},
&Role{},
&UserRole{},
&UserOrganization{},
&User{},
}
}
func main() {
ctx := context.Background()
// sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN("postgres://bun:bun@localhost:5433/bun?sslmode=disable")))
cfg, err := pgxpool.ParseConfig("postgres://bun:bun@localhost:5432/bun?sslmode=disable")
if err != nil {
panic(err)
}
cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
panic(err)
}
sqldb := stdlib.OpenDBFromPool(pool)
db := bun.NewDB(sqldb, pgdialect.New())
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"),
))
db.RegisterModel(RegisterEntities()...)
if err := db.ResetModel(ctx, RegisterEntities()...); err != nil {
panic(err)
}
OrgID := pulid.MustNew("orga")
BuID := pulid.MustNew("bua")
UserID := pulid.MustNew("ua")
RoleID := pulid.MustNew("ra")
{
bu := BusinessUnit{
ID: BuID,
Name: "bua",
}
if _, err := db.NewInsert().Model(&bu).Exec(ctx); err != nil {
panic(err)
}
org := Organization{
ID: OrgID,
BusinessUnitID: BuID,
Name: "orga",
}
if _, err := db.NewInsert().Model(&org).Exec(ctx); err != nil {
panic(err)
}
user := User{
ID: UserID,
BusinessUnitID: BuID,
CurrentOrganizationID: OrgID,
}
if _, err := db.NewInsert().Model(&user).Exec(ctx); err != nil {
panic(err)
}
role := Role{
ID: RoleID,
Name: "ra",
BusinessUnitID: BuID,
OrganizationID: OrgID,
}
if _, err := db.NewInsert().Model(&role).Exec(ctx); err != nil {
panic(err)
}
ur := UserRole{
UserID: UserID,
RoleID: RoleID,
BusinessUnitID: BuID,
OrganizationID: OrgID,
}
if _, err := db.NewInsert().Model(&ur).Exec(ctx); err != nil {
panic(err)
}
}
{
user := User{}
if err := db.NewSelect().
Model(&user).
Where("usr.id = ?", UserID).
Relation("Roles").
Relation("Roles.Permissions").
Scan(ctx); err != nil {
panic(err)
}
fmt.Println(user)
}
{
user := User{}
if err := db.NewSelect().
Model(&user).
Where("usr.id = ?", UserID).
Relation("Roles").
Relation("Roles.Permissions").
Scan(ctx); err != nil {
panic(err)
}
fmt.Println(user)
}
} |
Thank you for helping to identify the issue. I believe your judgment is correct. |
Thank you for going down this rabbit hole with me and also submitting the PR to fix. Really appreciate your responses to this issue. |
Description
After upgrading from bun v1.2.6 to bun v1.2.7 there seems to be a regression when it comes to m2m relation detection
Reproduction
Expected Behavior
The User base model should not be detected as a m2m relation to Roles.
Actual Behavior
The user base model is being detected as m2m relation directly to Roles and causing the following error.
The text was updated successfully, but these errors were encountered: