-
Notifications
You must be signed in to change notification settings - Fork 82
Description
Description
The generated factory code's FromExisting* methods cause infinite recursion and stack overflow when using models that were created with bidirectional relationships populated in their .R struct.
Environment
- Bob version: v0.41.1
- Database: PostgreSQL
- Go version: 1.23+
Database Schema
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE items (
id SERIAL PRIMARY KEY,
order_id INT NULL,
description TEXT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
CREATE TABLE item_details (
id SERIAL PRIMARY KEY,
item_id INT NOT NULL,
details TEXT NOT NULL,
FOREIGN KEY (item_id) REFERENCES items(id)
);Problem
When an Order is created with items using the factory's relationship helpers, the bidirectional relationships are automatically populated:
orderDao.R.Items[0]contains the itemorderDao.R.Items[0].R.Orderpoints back toorderDao
When attempting to use this item with a WithExisting* modifier:
// Create order with items
orderDao := factory.NewOrder(
dbfactory.OrderMods.AddNewItems(2),
).CreateOrFail(ctx, t, db)
// This causes stack overflow because orderDao.R.Items[0].R.Order points back to orderDao
factory.NewItemDetail(
dbfactory.ItemDetailMods.WithExistingItem(orderDao.R.Items[0])
).CreateOrFail(ctx, t, db)This results in:
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x14031200380 stack=[0x14031200000, 0x14051200000]
fatal error: stack overflow
Root Cause
The generated FromExisting* methods recursively load ALL relationships without checking for circular references:
func (f *Factory) FromExistingItem(m *models.Item) *ItemTemplate {
o := &ItemTemplate{f: f, alreadyPersisted: true}
// ... field assignments ...
ctx := context.Background()
if m.R.Order != nil {
ItemMods.WithExistingOrder(m.R.Order).Apply(ctx, o) // ← Calls FromExistingOrder
}
}
func (f *Factory) FromExistingOrder(m *models.Order) *OrderTemplate {
o := &OrderTemplate{f: f, alreadyPersisted: true}
// ... field assignments ...
ctx := context.Background()
if len(m.R.Items) > 0 {
OrderMods.AddExistingItems(m.R.Items...).Apply(ctx, o) // ← Calls FromExistingItem for each
}
}This creates an infinite loop: Item → Order → Items → Order → ∞
The issue occurs because:
orderDao.R.Items[0]has itsR.Orderfield populated (pointing back toorderDao)WithExistingItem()internally callsFromExistingItem()FromExistingItem()seesm.R.Order != niland callsWithExistingOrder(m.R.Order)WithExistingOrder()callsFromExistingOrder()FromExistingOrder()seesm.R.Itemsand callsAddExistingItems()for each item- This loops back to step 2, creating infinite recursion
Current Workaround
Manually break the circular reference before using the model:
orderDao.R.Items[0].R.Order = nil
factory.NewItemDetail(
dbfactory.ItemDetailMods.WithExistingItem(orderDao.R.Items[0])
).CreateOrFail(ctx, t, db)Or re-query the model without preloading relationships:
itemsDao, err := dbmodels.Items.Query(
dbmodels.SelectWhere.Items.OrderID.EQ(orderDao.ID),
// Don't preload relationships - this prevents R.Order from being populated
).All(ctx, db)
require.NoError(t, err)
factory.NewItemDetail(
dbfactory.ItemDetailMods.WithExistingItem(itemsDao[0])
).CreateOrFail(ctx, t, db)Additional Context
This issue affects any schema with bidirectional relationships:
- Parent-Child relationships
- Many-to-many relationships with join tables
- Any circular relationship patterns
The issue is particularly problematic in test code where factories are heavily used with pre-existing test data that has relationships already loaded in the .R struct.