Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions schema/issue1243_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package schema

import (
"reflect"
"testing"

"github.com/stretchr/testify/require"
)

// Models for TestNestedRelationWithSharedComposition (issue #1243).
// These must be at package level to allow circular type references
// between Business and Lead via shared embedded relation structs.

type idField struct {
ID int64 `bun:",pk,autoincrement"`
}

type business struct {
idField
leadRelation
}

type businessRelation struct {
BusinessID int64 `bun:",nullzero"`
Business *business `bun:"rel:belongs-to,join:business_id=id"`
}

type lead struct {
idField
businessRelation
}

type leadRelation struct {
LeadID int64 `bun:",nullzero"`
Lead *lead `bun:"rel:belongs-to,join:lead_id=id"`
}

type agent struct {
idField
businessRelation

Associations []*link `bun:"rel:has-many,join:id=agent_id"`
}

type agentRelation struct {
AgentID int64 `bun:",nullzero"`
Agent *agent `bun:"rel:belongs-to,join:agent_id=id"`
}

type link struct {
idField
agentRelation
leadRelation
}

func TestNestedRelationWithSharedComposition(t *testing.T) {
dialect := newNopDialect()
tables := NewTables(dialect)

// Initialize Link table, which triggers circular initialization:
// Link -> AgentRelation -> Agent -> BusinessRelation -> Business
// -> LeadRelation -> Lead -> BusinessRelation -> Business (cycle)
linkTable := tables.Get(reflect.TypeFor[*link]())

require.Contains(t, linkTable.StructMap, "lead",
"Link should have 'lead' in StructMap from embedded LeadRelation")

leadTable := linkTable.StructMap["lead"].Table
require.Contains(t, leadTable.StructMap, "business",
"Lead should have 'business' in StructMap from embedded BusinessRelation")

// This is the lookup that fails in issue #1243:
// when scanning column "lead__business__id" from a nested Relation() query.
field := linkTable.LookupField("lead__business__id")
require.NotNil(t, field,
"LookupField('lead__business__id') must resolve through Lead -> Business")
}
15 changes: 13 additions & 2 deletions schema/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ type Table struct {
SoftDeleteField *Field
UpdateSoftDeleteField func(fv reflect.Value, tm time.Time) error

flags internal.Flag
flags internal.Flag
initStarted bool
}

type structField struct {
Expand All @@ -78,6 +79,10 @@ type structField struct {
}

func (table *Table) init(dialect Dialect, typ reflect.Type) {
if table.initStarted {
return
}
table.initStarted = true
table.dialect = dialect
table.Type = typ
table.ZeroValue = reflect.New(table.Type).Elem()
Expand Down Expand Up @@ -233,10 +238,16 @@ func (t *Table) processFields(typ reflect.Type) {
if t.StructMap == nil {
t.StructMap = make(map[string]*structField)
}
// Register the StructMap entry with a placeholder table BEFORE
// triggering recursive initialization. This ensures that types in
// a circular dependency can find this entry during their own
// processFields, even if this table's init hasn't completed yet.
placeholder := t.dialect.Tables().Placeholder(field.IndirectType)
t.StructMap[field.Name] = &structField{
Index: field.Index,
Table: t.dialect.Tables().InProgress(field.IndirectType),
Table: placeholder,
}
t.dialect.Tables().InProgress(field.IndirectType)
}
}

Expand Down
22 changes: 16 additions & 6 deletions schema/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,25 @@ func (t *Tables) Get(typ reflect.Type) *Table {
return table
}

// InProgress returns the in-progress table for typ, initializing it
// if it has not been initialized yet. The Placeholder + init() split
// lets callers register StructMap entries with the table pointer
// before triggering recursive initialization, preventing missing
// entries in circular dependency chains.
func (t *Tables) InProgress(typ reflect.Type) *Table {
if table, ok := t.inProgress[typ]; ok {
return table
}

table := new(Table)
t.inProgress[typ] = table
table := t.Placeholder(typ)
table.init(t.dialect, typ)
return table
}

// Placeholder returns an existing in-progress table or creates a new
// uninitialized placeholder entry.
func (t *Tables) Placeholder(typ reflect.Type) *Table {
Comment thread
bevzzz marked this conversation as resolved.
table, ok := t.inProgress[typ]
if !ok {
table = new(Table)
t.inProgress[typ] = table
}
return table
}

Expand Down
Loading