Skip to content

Stack Overflow in Factory FromExisting* Methods Due to Circular References #584

@arthurspa

Description

@arthurspa

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 item
  • orderDao.R.Items[0].R.Order points back to orderDao

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:

  1. orderDao.R.Items[0] has its R.Order field populated (pointing back to orderDao)
  2. WithExistingItem() internally calls FromExistingItem()
  3. FromExistingItem() sees m.R.Order != nil and calls WithExistingOrder(m.R.Order)
  4. WithExistingOrder() calls FromExistingOrder()
  5. FromExistingOrder() sees m.R.Items and calls AddExistingItems() for each item
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions