Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 issue1243IDField struct {
Comment thread
bevzzz marked this conversation as resolved.
Outdated
ID int64 `bun:",pk,autoincrement"`
}

type issue1243Business struct {
issue1243IDField
issue1243LeadRelation
}

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

type issue1243Lead struct {
issue1243IDField
issue1243BusinessRelation
}

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

type issue1243Agent struct {
issue1243IDField
issue1243BusinessRelation

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

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

type issue1243Link struct {
issue1243IDField
issue1243AgentRelation
issue1243LeadRelation
}

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[*issue1243Link]())

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")
}
11 changes: 9 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 Down Expand Up @@ -233,10 +234,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
19 changes: 19 additions & 0 deletions schema/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,35 @@ func (t *Tables) Get(typ reflect.Type) *Table {

func (t *Tables) InProgress(typ reflect.Type) *Table {
if table, ok := t.inProgress[typ]; ok {
if !table.initStarted {
table.initStarted = true
table.init(t.dialect, typ)
}
return table
}

table := new(Table)
table.initStarted = true
t.inProgress[typ] = table
table.init(t.dialect, typ)

return table
}

// Placeholder returns an existing in-progress table or creates a new
// uninitialized placeholder entry. This allows callers to register
// StructMap entries with the table pointer before triggering recursive
// initialization via InProgress, preventing missing entries in circular
// dependency chains.
func (t *Tables) Placeholder(typ reflect.Type) *Table {
Comment thread
bevzzz marked this conversation as resolved.
if table, ok := t.inProgress[typ]; ok {
return table
}
table := new(Table)
t.inProgress[typ] = table
return table
}

// ByModel gets the table by its Go name.
func (t *Tables) ByModel(name string) *Table {
var found *Table
Expand Down
Loading