Repository: litestar-org/polyfactory (python)

Issue:
Support for relationship building models in `SQLAlchemyFactory`
### Summary

Suppose I have a series of cascading models like A, B, C. When I call the factory for A and enable the `__set_relationships__ `parameter, the factory will build model A and have B in each A, but no C in B.
just like:
A
|-B1
    |- Expected C but did not
|-B2
    |- Expected C but did not

I went and read the source code and tried to modify it, and found that it was because the `__set_relationships__` parameter was not set when recursively constructing the factory. When I tried to set the parameter manually it triggered a recursive error, and this feature may require a larger change to the structure of the code.

I am trying to use this library for database mock fake data, if implemented this feature will be able to stage my work.

### Basic Example

Here's a sample test
```py
class Author(Base):
    __tablename__ = "authors"

    id: Any = Column(Integer(), primary_key=True)
    books: Any = orm.relationship(
        "Book",
        uselist=True,
        back_populates="author",
    )


class Book(Base):
    __tablename__ = "books"

    id: Any = Column(Integer(), primary_key=True)
    author_id: Any = Column(
        Integer(),
        ForeignKey(Author.id),
        nullable=False,
    )
    author: Any = orm.relationship(
        Author,
        uselist=False,
        back_populates="books",
    )
    comments: Any = orm.relationship(
        "Comment",
        uselist=True,
        back_populates="book",
    )


class Comment(Base):
    __tablename__ = "comments"

    id: Any = Column(Integer(), primary_key=True)
    book_id: Any = Column(
        Integer(),
        ForeignKey(Book.id),
        nullable=False,
    )
    book: Any = orm.relationship(
        Book,
        uselist=False,
        back_populates="comments",
    )

def test_recursive_relationship_resolution() -> None:
    class AuthorFactory(SQLAlchemyFactory[Author]):
        __model__ = Author
        __set_relationships__ = True

    result = AuthorFactory.build()
    assert isinstance(result.books, list)
    assert isinstance(result.books[0], Book)
    assert isinstance(result.books[0].comments, list)
    assert isinstance(result.books[0].comments[0], Comment)
```

### Drawbacks and Impact

I try add code in 
https://github.com/litestar-org/polyfactory/blob/8dc8e1a4594a75ad9a16e1b6f5041b6044fc4f51/polyfactory/factories/base.py#L650C1-L650C80

like that
```py
if BaseFactory.is_batch_factory_type(annotation=unwrapped_annotation):
    factory = cls._get_or_create_factory(model=field_meta.type_args[0])
    if hasattr(factory, '__set_relationships__'):
        setattr(factory, '__set_relationships__', getattr(cls, '__set_relationships__'))
    ......
```
This only solves recursive construction in one-to-many or many-to-many relationships and is very inelegant, raising recursion errors when I try to extend it to one-to-one or many-to-one relationships.

like that
```py
if BaseFactory.is_factory_type(annotation=unwrapped_annotation):
    factory = cls._get_or_create_factory(model=unwrapped_annotation)
    if hasattr(factory, '__set_relationships__'):
        setattr(factory, '__set_relationships__', getattr(cls, '__set_relationships__'))

    return factory.build(
        **(field_build_parameters if isinstance(field_build_parameters, Mapping) else {}),
    )
```


### Unresolved questions

_No response_

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/polyfactory/issues/467">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/polyfactory/issues/467/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/polyfactory/issues/467/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->


Pull request description:
feat: add recursion guard
<!--
By submitting this pull request, you agree to:
- follow [Litestar's Code of Conduct](https://github.com/litestar-org/.github/blob/main/CODE_OF_CONDUCT.md)
- follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md)
- follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/)
-->

### Pull Request Checklist

- [x] New code has 100% test coverage
- [ ] (If applicable) The prose documentation has been updated to reflect the changes introduced by this PR
- [ ] (If applicable) The reference documentation has been updated to reflect the changes introduced by this PR
- [x] Pre-Commit Checks were ran and passed
- [x] Tests were ran and passed

### Description
<!--
Please describe your pull request for new release changelog purposes
-->

- Add new new arg `build_context` to build related methods
- Track seen models in this parameter to guard against recursion
- Note this may arguably be backwards breaking but would expect recursion errors so fine to add as a default. Happy to add a configuration instead if preferable

### Close Issue(s)
<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234
-->

- Fixes #467 


Patch:
diff --git a/docs/usage/library_factories/sqlalchemy_factory.rst b/docs/usage/library_factories/sqlalchemy_factory.rst
index ef864dc..ee72c9e 100644
--- a/docs/usage/library_factories/sqlalchemy_factory.rst
+++ b/docs/usage/library_factories/sqlalchemy_factory.rst
@@ -37,6 +37,16 @@ By default, this will add generated models to the session and then commit. This
 Similarly for ``__async_session__`` and ``create_async``.
 
 
+Adding global overrides
+------------------------------
+
+By combining the above and using other settings, a global base factory can be set up for other factories.
+
+.. literalinclude:: /examples/library_factories/sqlalchemy_factory/test_example_4.py
+    :caption: Using persistence
+    :language: python
+
+
 API reference
 ------------------------------
 Full API docs are available :class:`here <polyfactory.factories.sqlalchemy_factory.SQLAlchemyFactory>`.
diff --git a/polyfactory/factories/base.py b/polyfactory/factories/base.py
index 71b0407..9ab29c0 100644
--- a/polyfactory/factories/base.py
+++ b/polyfactory/factories/base.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import copy
 from abc import ABC, abstractmethod
 from collections import Counter, abc, deque
 from contextlib import suppress
@@ -41,6 +42,7 @@ from typing import (
     Mapping,
     Sequence,
     Type,
+    TypedDict,
     TypeVar,
     cast,
 )
@@ -65,14 +67,7 @@ from polyfactory.utils.helpers import (
     unwrap_optional,
 )
 from polyfactory.utils.model_coverage import CoverageContainer, CoverageContainerCallable, resolve_kwargs_coverage
-from polyfactory.utils.predicates import (
-    get_type_origin,
-    is_any,
-    is_literal,
-    is_optional,
-    is_safe_subclass,
-    is_union,
-)
+from polyfactory.utils.predicates import get_type_origin, is_any, is_literal, is_optional, is_safe_subclass, is_union
 from polyfactory.value_generators.complex_types import handle_collection_type, handle_collection_type_coverage
 from polyfactory.value_generators.constrained_collections import (
     handle_constrained_collection,
@@ -88,11 +83,7 @@ from polyfactory.value_generators.constrained_path import handle_constrained_pat
 from polyfactory.value_generators.constrained_strings import handle_constrained_string_or_bytes
 from polyfactory.value_generators.constrained_url import handle_constrained_url
 from polyfactory.value_generators.constrained_uuid import handle_constrained_uuid
-from polyfactory.value_generators.primitives import (
-    create_random_boolean,
-    create_random_bytes,
-    create_random_string,
-)
+from polyfactory.value_generators.primitives import create_random_boolean, create_random_bytes, create_random_string
 
 if TYPE_CHECKING:
     from typing_extensions import TypeGuard
@@ -105,6 +96,17 @@ T = TypeVar("T")
 F = TypeVar("F", bound="BaseFactory[Any]")
 
 
+class BuildContext(TypedDict):
+    seen_models: set[type]
+
+
+def _get_build_context(build_context: BuildContext | None) -> BuildContext:
+    if build_context is None:
+        return {"seen_models": set()}
+
+    return copy.deepcopy(build_context)
+
+
 class BaseFactory(ABC, Generic[T]):
     """Base Factory class - this class holds the main logic of the library"""
 
@@ -277,7 +279,12 @@ class BaseFactory(ABC, Generic[T]):
         )
 
     @classmethod
-    def _handle_factory_field(cls, field_value: Any, field_build_parameters: Any | None = None) -> Any:
+    def _handle_factory_field(
+        cls,
+        field_value: Any,
+        build_context: BuildContext,
+        field_build_parameters: Any | None = None,
+    ) -> Any:
         """Handle a value defined on the factory class itself.
 
         :param field_value: A value defined as an attribute on the factory class.
@@ -287,12 +294,14 @@ class BaseFactory(ABC, Generic[T]):
         """
         if is_safe_subclass(field_value, BaseFactory):
             if isinstance(field_build_parameters, Mapping):
-                return field_value.build(**field_build_parameters)
+                return field_value.build(_build_context=build_context, **field_build_parameters)
 
             if isinstance(field_build_parameters, Sequence):
-                return [field_value.build(**parameter) for parameter in field_build_parameters]
+                return [
+                    field_value.build(_build_context=build_context, **parameter) for parameter in field_build_parameters
+                ]
 
-            return field_value.build()
+            return field_value.build(_build_context=build_context)
 
         if isinstance(field_value, Use):
             return field_value.to_value()
@@ -303,7 +312,12 @@ class BaseFactory(ABC, Generic[T]):
         return field_value() if callable(field_value) else field_value
 
     @classmethod
-    def _handle_factory_field_coverage(cls, field_value: Any, field_build_parameters: Any | None = None) -> Any:
+    def _handle_factory_field_coverage(
+        cls,
+        field_value: Any,
+        field_build_parameters: Any | None = None,
+        build_context: BuildContext | None = None,
+    ) -> Any:
         """Handle a value defined on the factory class itself.
 
         :param field_value: A value defined as an attribute on the factory class.
@@ -313,10 +327,13 @@ class BaseFactory(ABC, Generic[T]):
         """
         if is_safe_subclass(field_value, BaseFactory):
             if isinstance(field_build_parameters, Mapping):
-                return CoverageContainer(field_value.coverage(**field_build_parameters))
+                return CoverageContainer(field_value.coverage(_build_context=build_context, **field_build_parameters))
 
             if isinstance(field_build_parameters, Sequence):
-                return [CoverageContainer(field_value.coverage(**parameter)) for parameter in field_build_parameters]
+                return [
+                    CoverageContainer(field_value.coverage(_build_context=build_context, **parameter))
+                    for parameter in field_build_parameters
+                ]
 
             return CoverageContainer(field_value.coverage())
 
@@ -621,15 +638,18 @@ class BaseFactory(ABC, Generic[T]):
         cls,
         field_meta: FieldMeta,
         field_build_parameters: Any | None = None,
+        build_context: BuildContext | None = None,
     ) -> Any:
         """Return a field value on the subclass if existing, otherwise returns a mock value.
 
         :param field_meta: FieldMeta instance.
         :param field_build_parameters: Any build parameters passed to the factory as kwarg values.
+        :param build_context: BuildContext data for current build.
 
         :returns: An arbitrary value.
 
         """
+        build_context = _get_build_context(build_context)
         if cls.is_ignored_type(field_meta.annotation):
             return None
 
@@ -648,20 +668,32 @@ class BaseFactory(ABC, Generic[T]):
             return cls.get_constrained_field_value(annotation=unwrapped_annotation, field_meta=field_meta)
 
         if BaseFactory.is_factory_type(annotation=unwrapped_annotation):
+            if not field_build_parameters and unwrapped_annotation in build_context["seen_models"]:
+                return None if is_optional(field_meta.annotation) else Null
+
             return cls._get_or_create_factory(model=unwrapped_annotation).build(
+                _build_context=build_context,
                 **(field_build_parameters if isinstance(field_build_parameters, Mapping) else {}),
             )
 
         if BaseFactory.is_batch_factory_type(annotation=unwrapped_annotation):
             factory = cls._get_or_create_factory(model=field_meta.type_args[0])
             if isinstance(field_build_parameters, Sequence):
-                return [factory.build(**field_parameters) for field_parameters in field_build_parameters]
+                return [
+                    factory.build(_build_context=build_context, **field_parameters)
+                    for field_parameters in field_build_parameters
+                ]
+
+            if field_meta.type_args[0] in build_context["seen_models"]:
+                return []
+
             if not cls.__randomize_collection_length__:
-                return [factory.build()]
+                return [factory.build(_build_context=build_context)]
+
             batch_size = cls.__random__.randint(cls.__min_collection_length__, cls.__max_collection_length__)
-            return factory.batch(size=batch_size)
+            return factory.batch(size=batch_size, _build_context=build_context)
 
-        if (origin := get_type_origin(unwrapped_annotation)) and issubclass(origin, Collection):
+        if (origin := get_type_origin(unwrapped_annotation)) and is_safe_subclass(origin, Collection):
             if cls.__randomize_collection_length__:
                 collection_type = get_collection_type(unwrapped_annotation)
                 if collection_type != dict:
@@ -682,8 +714,9 @@ class BaseFactory(ABC, Generic[T]):
 
             return handle_collection_type(field_meta, origin, cls)
 
-        if is_union(field_meta.annotation) and field_meta.children:
-            return cls.get_field_value(cls.__random__.choice(field_meta.children))
+        if is_union(unwrapped_annotation) and field_meta.children:
+            children = [child for child in field_meta.children if child.annotation not in build_context["seen_models"]]
+            return cls.get_field_value(cls.__random__.choice(children))
 
         if is_any(unwrapped_annotation) or isinstance(unwrapped_annotation, TypeVar):
             return create_random_string(cls.__random__, min_length=1, max_length=10)
@@ -707,11 +740,13 @@ class BaseFactory(ABC, Generic[T]):
         cls,
         field_meta: FieldMeta,
         field_build_parameters: Any | None = None,
+        build_context: BuildContext | None = None,
     ) -> Iterable[Any]:
         """Return a field value on the subclass if existing, otherwise returns a mock value.
 
         :param field_meta: FieldMeta instance.
         :param field_build_parameters: Any build parameters passed to the factory as kwarg values.
+        :param build_context: BuildContext data for current build.
 
         :returns: An iterable of values.
 
@@ -739,6 +774,7 @@ class BaseFactory(ABC, Generic[T]):
             elif BaseFactory.is_factory_type(annotation=unwrapped_annotation):
                 yield CoverageContainer(
                     cls._get_or_create_factory(model=unwrapped_annotation).coverage(
+                        _build_context=build_context,
                         **(field_build_parameters if isinstance(field_build_parameters, Mapping) else {}),
                     ),
                 )
@@ -861,6 +897,9 @@ class BaseFactory(ABC, Generic[T]):
         :returns: A dictionary of build results.
 
         """
+        _build_context = _get_build_context(kwargs.pop("_build_context", None))
+        _build_context["seen_models"].add(cls.__model__)
+
         result: dict[str, Any] = {**kwargs}
         generate_post: dict[str, PostGenerated] = {}
 
@@ -883,10 +922,19 @@ class BaseFactory(ABC, Generic[T]):
                     result[field_meta.name] = cls._handle_factory_field(
                         field_value=field_value,
                         field_build_parameters=field_build_parameters,
+                        build_context=_build_context,
                     )
                     continue
 
-                result[field_meta.name] = cls.get_field_value(field_meta, field_build_parameters=field_build_parameters)
+                field_result = cls.get_field_value(
+                    field_meta,
+                    field_build_parameters=field_build_parameters,
+                    build_context=_build_context,
+                )
+                if field_result is Null:
+                    continue
+
+                result[field_meta.name] = field_result
 
         for field_name, post_generator in generate_post.items():
             result[field_name] = post_generator.to_value(field_name, result)
@@ -898,10 +946,14 @@ class BaseFactory(ABC, Generic[T]):
         """Process the given kwargs and generate values for the factory's model.
 
         :param kwargs: Any build kwargs.
+        :param build_context: BuildContext data for current build.
 
         :returns: A dictionary of build results.
 
         """
+        _build_context = _get_build_context(kwargs.pop("_build_context", None))
+        _build_context["seen_models"].add(cls.__model__)
+
         result: dict[str, Any] = {**kwargs}
         generate_post: dict[str, PostGenerated] = {}
 
@@ -925,11 +977,16 @@ class BaseFactory(ABC, Generic[T]):
                     result[field_meta.name] = cls._handle_factory_field_coverage(
                         field_value=field_value,
                         field_build_parameters=field_build_parameters,
+                        build_context=_build_context,
                     )
                     continue
 
                 result[field_meta.name] = CoverageContainer(
-                    cls.get_field_value_coverage(field_meta, field_build_parameters=field_build_parameters),
+                    cls.get_field_value_coverage(
+                        field_meta,
+                        field_build_parameters=field_build_parameters,
+                        build_context=_build_context,
+                    ),
                 )
 
         for resolved in resolve_kwargs_coverage(result):
@@ -946,7 +1003,6 @@ class BaseFactory(ABC, Generic[T]):
         :returns: An instance of type T.
 
         """
-
         return cast("T", cls.__model__(**cls.process_kwargs(**kwargs)))
 
     @classmethod
diff --git a/polyfactory/factories/beanie_odm_factory.py b/polyfactory/factories/beanie_odm_factory.py
index 8f98a5c..ddd3169 100644
--- a/polyfactory/factories/beanie_odm_factory.py
+++ b/polyfactory/factories/beanie_odm_factory.py
@@ -12,6 +12,7 @@ from polyfactory.utils.predicates import is_safe_subclass
 if TYPE_CHECKING:
     from typing_extensions import TypeGuard
 
+    from polyfactory.factories.base import BuildContext
     from polyfactory.field_meta import FieldMeta
 
 try:
@@ -55,7 +56,12 @@ class BeanieDocumentFactory(Generic[T], ModelFactory[T]):
         return is_safe_subclass(value, Document)
 
     @classmethod
-    def get_field_value(cls, field_meta: "FieldMeta", field_build_parameters: Any | None = None) -> Any:
+    def get_field_value(
+        cls,
+        field_meta: "FieldMeta",
+        field_build_parameters: Any | None = None,
+        build_context: BuildContext | None = None,
+    ) -> Any:
         """Return a field value on the subclass if existing, otherwise returns a mock value.
 
         :param field_meta: FieldMeta instance.
@@ -74,4 +80,8 @@ class BeanieDocumentFactory(Generic[T], ModelFactory[T]):
                 field_meta.annotation = link_class
                 field_meta.annotation = link_class
 
-        return super().get_field_value(field_meta=field_meta, field_build_parameters=field_build_parameters)
+        return super().get_field_value(
+            field_meta=field_meta,
+            field_build_parameters=field_build_parameters,
+            build_context=build_context,
+        )
diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py
index 64102e2..16218dc 100644
--- a/polyfactory/factories/pydantic_factory.py
+++ b/polyfactory/factories/pydantic_factory.py
@@ -368,7 +368,11 @@ class ModelFactory(Generic[T], BaseFactory[T]):
         return super().get_constrained_field_value(annotation, field_meta)
 
     @classmethod
-    def build(cls, factory_use_construct: bool = False, **kwargs: Any) -> T:
+    def build(
+        cls,
+        factory_use_construct: bool = False,
+        **kwargs: Any,
+    ) -> T:
         """Build an instance of the factory's __model__
 
         :param factory_use_construct: A boolean that determines whether validations will be made when instantiating the
diff --git a/polyfactory/utils/helpers.py b/polyfactory/utils/helpers.py
index c28c062..a6432b5 100644
--- a/polyfactory/utils/helpers.py
+++ b/polyfactory/utils/helpers.py
@@ -11,13 +11,7 @@ except ImportError:
 from typing_extensions import get_args, get_origin
 
 from polyfactory.constants import TYPE_MAPPING
-from polyfactory.utils.predicates import (
-    is_annotated,
-    is_new_type,
-    is_optional,
-    is_safe_subclass,
-    is_union,
-)
+from polyfactory.utils.predicates import is_annotated, is_new_type, is_optional, is_safe_subclass, is_union
 
 if TYPE_CHECKING:
     from random import Random
@@ -71,15 +65,14 @@ def unwrap_annotation(annotation: Any, random: Random) -> Any:
     :returns: The unwrapped annotation.
 
     """
-    while is_optional(annotation) or is_union(annotation) or is_new_type(annotation) or is_annotated(annotation):
+    while is_optional(annotation) or is_new_type(annotation) or is_annotated(annotation):
         if is_new_type(annotation):
             annotation = unwrap_new_type(annotation)
         elif is_optional(annotation):
             annotation = unwrap_optional(annotation)
         elif is_annotated(annotation):
             annotation = unwrap_annotated(annotation, random=random)[0]
-        else:
-            annotation = unwrap_union(annotation, random=random)
+
     return annotation
 
 


Test patch:
diff --git a/docs/examples/library_factories/sqlalchemy_factory/conftest.py b/docs/examples/library_factories/sqlalchemy_factory/conftest.py
new file mode 100644
index 0000000..2227e64
--- /dev/null
+++ b/docs/examples/library_factories/sqlalchemy_factory/conftest.py
@@ -0,0 +1,11 @@
+from collections.abc import Iterable
+
+import pytest
+
+from docs.examples.library_factories.sqlalchemy_factory.test_example_4 import BaseFactory
+
+
+@pytest.fixture(scope="module")
+def _remove_default_factories() -> Iterable[None]:
+    yield
+    BaseFactory._base_factories.remove(BaseFactory)  # noqa: SLF001
diff --git a/docs/examples/library_factories/sqlalchemy_factory/test_example_4.py b/docs/examples/library_factories/sqlalchemy_factory/test_example_4.py
new file mode 100644
index 0000000..0d616a3
--- /dev/null
+++ b/docs/examples/library_factories/sqlalchemy_factory/test_example_4.py
@@ -0,0 +1,58 @@
+from typing import List
+
+from sqlalchemy import ForeignKey, create_engine
+from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
+
+from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory, T
+
+
+class Base(DeclarativeBase):
+    ...
+
+
+class Author(Base):
+    __tablename__ = "authors"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    name: Mapped[str]
+
+    books: Mapped[List["Book"]] = relationship(
+        "Book",
+        uselist=True,
+        back_populates="author",
+    )
+
+
+class Book(Base):
+    __tablename__ = "books"
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    author_id: Mapped[int] = mapped_column(ForeignKey(Author.id), nullable=False)
+    author: Mapped[Author] = relationship(
+        "Author",
+        uselist=False,
+        back_populates="books",
+    )
+
+
+class BaseFactory(SQLAlchemyFactory[T]):
+    __is_base_factory__ = True
+    __set_relationships__ = True
+    __randomize_collection_length__ = True
+    __min_collection_length__ = 3
+
+
+def test_custom_sqla_factory() -> None:
+    engine = create_engine("sqlite:///:memory:")
+    Base.metadata.create_all(engine)
+    session = Session(engine)
+
+    BaseFactory.__session__ = session  # Or using a callable that returns a session
+
+    author = BaseFactory.create_factory(Author).create_sync()
+    assert author.id is not None
+    assert author.id == author.books[0].author_id
+
+    book = BaseFactory.create_factory(Book).create_sync()
+    assert book.id is not None
+    assert book.author.books == [book]
diff --git a/tests/sqlalchemy_factory/test_sqlalchemy_factory_common.py b/tests/sqlalchemy_factory/test_sqlalchemy_factory_common.py
index 2bb7873..8cdcf66 100644
--- a/tests/sqlalchemy_factory/test_sqlalchemy_factory_common.py
+++ b/tests/sqlalchemy_factory/test_sqlalchemy_factory_common.py
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
 from sqlalchemy.orm.decl_api import DeclarativeMeta, registry
 
 from polyfactory.exceptions import ConfigurationException
+from polyfactory.factories.base import BaseFactory
 from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory
 
 
@@ -198,6 +199,26 @@ def test_relationship_list_resolution() -> None:
     assert isinstance(result.books[0], Book)
 
 
+def test_sqla_factory_create() -> None:
+    engine = create_engine("sqlite:///:memory:")
+    Base.metadata.create_all(engine)
+
+    class OverridenSQLAlchemyFactory(SQLAlchemyFactory):
+        __is_base_factory__ = True
+        __session__ = Session(engine)
+        __set_relationships__ = True
+
+    author: Author = OverridenSQLAlchemyFactory.create_factory(Author).create_sync()
+    assert isinstance(author.books[0], Book)
+    assert author.books[0].author is author
+
+    book = OverridenSQLAlchemyFactory.create_factory(Book).create_sync()
+    assert book.author is not None
+    assert book.author.books == [book]
+
+    BaseFactory._base_factories.remove(OverridenSQLAlchemyFactory)
+
+
 async def test_invalid_peristence_config_raises() -> None:
     class AuthorFactory(SQLAlchemyFactory[Author]):
         __model__ = Author
diff --git a/tests/test_dicts.py b/tests/test_dicts.py
index 0eae3da..d9ad95b 100644
--- a/tests/test_dicts.py
+++ b/tests/test_dicts.py
@@ -36,10 +36,10 @@ def test_dict_with_union_random_types() -> None:
     class MyClassFactory(ModelFactory[MyClass]):
         __model__ = MyClass
 
-    MyClassFactory.seed_random(100)
+    MyClassFactory.seed_random(10)
 
     test_obj_1 = MyClassFactory.build()
     test_obj_2 = MyClassFactory.build()
 
-    assert isinstance(next(iter(test_obj_1.val.values())), str)
-    assert isinstance(next(iter(test_obj_2.val.values())), int)
+    assert isinstance(next(iter(test_obj_1.val.values())), int)
+    assert isinstance(next(iter(test_obj_2.val.values())), str)
diff --git a/tests/test_recursive_models.py b/tests/test_recursive_models.py
new file mode 100644
index 0000000..9f8c4ad
--- /dev/null
+++ b/tests/test_recursive_models.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import List, Optional, Union
+
+import pytest
+from pydantic import BaseModel, Field
+
+from polyfactory.factories.dataclass_factory import DataclassFactory
+from polyfactory.factories.pydantic_factory import ModelFactory
+
+
+class _Sentinel:
+    ...
+
+
+@dataclass
+class Node:
+    value: int
+    union_child: Union[Node, int]  # noqa: UP007
+    list_child: List[Node]  # noqa: UP006
+    optional_child: Optional[Node]  # noqa: UP007
+    child: Node = field(default=_Sentinel)  # type: ignore[assignment]
+
+    def __post_init__(self) -> None:
+        # Emulate recursive models set by external init, e.g. ORM relationships
+        if self.child is _Sentinel:  # type: ignore[comparison-overlap]
+            self.child = self
+
+
+def test_recusive_model() -> None:
+    factory = DataclassFactory.create_factory(Node)
+
+    result = factory.build()
+    assert result.child is result, "Default is not used"
+    assert isinstance(result.union_child, int)
+    assert result.optional_child is None
+    assert result.list_child == []
+
+    assert factory.build(child={"child": None}).child.child is None
+
+
+class PydanticNode(BaseModel):
+    value: int
+    union_child: Union[PydanticNode, int]  # noqa: UP007
+    list_child: List[PydanticNode]  # noqa: UP006
+    optional_child: Union[PydanticNode, None]  # noqa: UP007
+    child: PydanticNode = Field(default=_Sentinel)  # type: ignore[assignment]
+
+
+@pytest.mark.parametrize("factory_use_construct", (True, False))
+def test_recursive_pydantic_models(factory_use_construct: bool) -> None:
+    factory = ModelFactory.create_factory(PydanticNode)
+
+    result = factory.build(factory_use_construct)
+    assert result.child is _Sentinel, "Default is not used"
+    assert isinstance(result.union_child, int)
+    assert result.optional_child is None
+    assert result.list_child == []
+
+
+@dataclass
+class Author:
+    name: str
+    books: List[Book]  # noqa: UP006
+
+
+_DEFAULT_AUTHOR = Author(name="default", books=[])
+
+
+@dataclass
+class Book:
+    name: str
+    author: Author = field(default_factory=lambda: _DEFAULT_AUTHOR)
+
+
+def test_recusive_list_model() -> None:
+    factory = DataclassFactory.create_factory(Author)
+    assert factory.build().books[0].author is _DEFAULT_AUTHOR
+    assert factory.build(books=[]).books == []
+
+    book_factory = DataclassFactory.create_factory(Book)
+    assert book_factory.build().author.books == []
+    assert book_factory.build(author=None).author is None