Skip to content
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

Pass sequence to SubFactory #957

Open
mademovic opened this issue Jul 4, 2022 · 6 comments
Open

Pass sequence to SubFactory #957

mademovic opened this issue Jul 4, 2022 · 6 comments

Comments

@mademovic
Copy link

Description

When copying a sequence to a SubFactory, the value of the field on the child factory is always 0.

To Reproduce

Model / Factory code
class PositionFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.Position
        sqlalchemy_session_persistence = "commit"

    position_number = factory.Sequence(lambda n: n)
    position_account = factory.SubFactory(
        "factories.PositionAccountFactory",
        position_number=factory.SelfAttribute("..position_number"),
    )


class PositionAccountFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.PositionAccount
        sqlalchemy_session_persistence = "commit"

    payment_amount = 150.0
    position_number = None
The issue

The sequence counter is ignored and the PositionAccount.position_number is always 0.

@rbarrois
Copy link
Member

rbarrois commented Jul 4, 2022

Could you show how you call the PositionFactory()?
Do you have specific settings or hooks on the Position model?

I can't reproduce this issue with simple factories 🤔

@mademovic
Copy link
Author

mademovic commented Jul 5, 2022

I just call it like that: PositionFactory()
I use the primaryjoin kw argument for the relationships between the tables, because the underlying tables have no foreign keys set up. Other than that, there are no specific settings.

from sqlalchemy import Column
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.mysql import SMALLINT, DECIMAL, INTEGER

class Position(Base):
    id = Column(INTEGER(11), primary_key=True)
    position_number = Column(SMALLINT(6))
    position_account = relationship(
        "PositionAccount",
        primaryjoin="foreign(Position.position_number) == PositionAccount.position_number",
    )

class PositionAccount(Base):
    id = Column(INTEGER(11), primary_key=True)
    payment_amount = Column(DECIMAL(10, 2), nullable=False)

@kingbuzzman
Copy link
Contributor

kingbuzzman commented Jul 5, 2022

This should work.

class PositionFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.Position
        sqlalchemy_session_persistence = "commit"
        sqlalchemy_session = models.session

    position_number = factory.Sequence(lambda n: n)
    position_account = factory.SubFactory(
        PositionAccountFactory,
        position_number=factory.Sequence(lambda n: n)
    )

@rbarrois can expand on this more/ or just correct me. But I think since your relationship between Position and PositionAccount is position_number, I don't think factory.SelfAttribute(..) would ever work for you. Again, @rbarrois will give you better information.

^ Take this with a grain of salt


Changes I made, and tests:

class PositionAccount(Base):
    id = Column(INTEGER(11), primary_key=True)
    payment_amount = Column(DECIMAL(10, 2), nullable=False)
+   position_number = Column(SMALLINT(6))  # this was missing from your example, but you're referencing it

(Everything else from your example code)

class TestCase(TransactionTestCase):
    def test_sequence(self):
        position = PositionFactory()
        self.assertEqual(position.position_number, 0)
        self.assertEqual(position.position_account.position_number, 0)
        self.assertEqual(position.position_account.payment_amount, 150)

        position = PositionFactory()
        self.assertEqual(position.position_number, 1)
        self.assertEqual(position.position_account.position_number, 1)
        self.assertEqual(position.position_account.payment_amount, 150)

@rbarrois
Copy link
Member

rbarrois commented Jul 6, 2022

@kingbuzzman your example will only work as long as one never calls the PositionAccountFactory() or any factory's reset_sequence, as it relies on the fact that both sequences stay in sync.

When I write the code without SQLAlchemy, I see no issue:

from dataclasses import dataclass
from decimal import Decimal

import factory

@dataclass
class PositionAccount:
  position_number: int = None
  payment_amount: Decimal = Decimal()

@dataclass
class Position:
  position_number: int
  position_account: PositionAccount

class PositionAccountFactory(factory.Factory):
  position_number = None
  payment_amount = Decimal("150.0")
  class Meta:
    model = PositionAccount

class PositionFactory(factory.Factory):
  position_number = factory.Sequence(lambda n: n)
  position_account = factory.SubFactory(
    PositionAccountFactory,
    position_number=factory.SelfAttribute("..position_number"),
  )
  class Meta:
    model = Position

With that, if I perform the following:

p1, p2 = PositionFactory.create_batch(2)
pa3 = PositionAccountFactory()  # Shift the sequence on the position account
p4 = PositionFactory(position_number=24)
p5 = PositionFactory()
assert p1.position_number == 0
assert p2.position_number == 1
assert pa3.position_number is None
assert p4.position_number == 24
assert p5.position_number == 3  # One step of the sequence was consumed by p4

for p in [p1, p2, p4, p5]:
  assert p.position_number == p.position_account.position_number

@kingbuzzman
Copy link
Contributor

Ah I see what you mean, each Factory has its own sequence and I'm incrementing them in parallel, the second they fall behind for whatever reason, this whole thing falls apart -- right? That's what you're illustrating correct? How do you make it more robust? How can you use the sequence of the parent always?

@n1ngu
Copy link

n1ngu commented Sep 17, 2022

Mhm, is a mimimal reproductible case available? This does work with no problems

class A(DictFactory):
    x = 0

Class B(DictFactory):
    y = Sequence(int)
    a = SubFactory(A, x=SelfAttribute("..y"))

B.build()
B.build()

is there a chance the issue is in those sqla models @mademovic ? Did you ever try the sqlalchemy_session_persistence = "flush" meta parameter?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants