Repository: igorbenav/fastcrud (python)

Issue:
`nest_joins` only works if `join_prefix` is set
**Describe the bug or question**
Attributes from a joined model are not nested if no `join_prefix` is set

**To Reproduce**

Consider two simple models, `Hero` and `Ability`
```python
class Ability(Base, UUIDMixin, TimestampMixin, SoftDeleteMixin):
    __tablename__ = "abilities"

    name: Mapped[str] = mapped_column(nullable=False)
    strength: Mapped[int] = mapped_column(nullable=False)
    heroes: Mapped[list["Hero"]] = relationship(back_populates="ability")

class Hero(Base, UUIDMixin, TimestampMixin, SoftDeleteMixin):
    __tablename__ = "heroes"

    name: Mapped[str] = mapped_column(nullable=False)
    ability_id: Mapped[int] = mapped_column(ForeignKey("abilities.id"))
    ability: Mapped["Ability"] = relationship(back_populates="heroes")
```

Then, this code does not work as expected:

```python
heroes = await crud_hero.get_multi_joined(db, join_model=Ability, nest_joins=True)
```

Returns this (unrelated columns are removed):

```python
{
    "data": [
        {
            "name": "Diana",
            "ability_id": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
            "id": UUID("8212bccb-ce20-489a-a675-45772ad60eb8"),
            "name_1": "Superstrength",
            "strength": 10,
            "id_1": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
        },
    ],
    "total_count": 2,
}
```

When adding a prefix, it (kinda) works:

```python
heroes = await crud_hero.get_multi_joined(db, join_model=Ability, join_prefix="ability_", nest_joins=True)
```

Result is looking better

```python
{
    "data": [
        {
            "name": "Diana",
            "ability": {
                "id": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
                "name": "Superstrength",
                "strength": 10,
                "id_1": UUID("6e52176e-8a92-4a8d-b0b3-1fcd55acc666"),
            },
            "id": UUID("8212bccb-ce20-489a-a675-45772ad60eb8"),
        },
    ],
    "total_count": 2,
}
```



Pull request description:
Join fixes
Not done, but this will fix #64, #65 and #66

Patch:
diff --git a/docs/advanced/joins.md b/docs/advanced/joins.md
index b17f674..54745b6 100644
--- a/docs/advanced/joins.md
+++ b/docs/advanced/joins.md
@@ -65,7 +65,7 @@ For simpler join requirements, FastCRUD allows specifying join parameters direct
 # Fetch tasks with user details, specifying a left join
 tasks_with_users = await task_crud.get_joined(
     db=db,
-    model=User,
+    join_model=User,
     join_on=Task.user_id == User.id,
     join_type="left"
 )
@@ -94,7 +94,7 @@ And join them with `FastCRUD`:
 ```python
 user_tier = await user_crud.get_joined(
     db=db,
-    model=Tier,
+    join_model=Tier,
     join_on=User.tier_id == Tier.id,
     join_type="left",
     join_prefix="tier_",,
@@ -118,7 +118,7 @@ If you want the joined data in a nested dictionary instead, you may just pass `n
 ```python
 user_tier = await user_crud.get_joined(
     db=db,
-    model=Tier,
+    join_model=Tier,
     join_on=User.tier_id == Tier.id,
     join_type="left",
     join_prefix="tier_",
diff --git a/fastcrud/crud/fast_crud.py b/fastcrud/crud/fast_crud.py
index f813407..db03c53 100644
--- a/fastcrud/crud/fast_crud.py
+++ b/fastcrud/crud/fast_crud.py
@@ -271,6 +271,47 @@ class FastCRUD(
 
         return stmt
 
+    def _prepare_and_apply_joins(
+        self,
+        stmt: Select,
+        joins_config: list[JoinConfig],
+        use_temporary_prefix: bool = False,
+    ):
+        """
+        Applies joins to the given SQL statement based on a list of JoinConfig objects.
+
+        Args:
+            stmt: The initial SQL statement.
+            joins_config: Configurations for all joins.
+            use_temporary_prefix: Whether to use or not an aditional prefix for joins. Default False.
+
+        Returns:
+            Select: The modified SQL statement with joins applied.
+        """
+        for join in joins_config:
+            model = join.alias or join.model
+            join_select = _extract_matching_columns_from_schema(
+                model,
+                join.schema_to_select,
+                join.join_prefix,
+                join.alias,
+                use_temporary_prefix,
+            )
+            joined_model_filters = self._parse_filters(
+                model=model, **(join.filters or {})
+            )
+
+            if join.join_type == "left":
+                stmt = stmt.outerjoin(model, join.join_on).add_columns(*join_select)
+            elif join.join_type == "inner":
+                stmt = stmt.join(model, join.join_on).add_columns(*join_select)
+            else:
+                raise ValueError(f"Unsupported join type: {join.join_type}.")
+            if joined_model_filters:
+                stmt = stmt.filter(*joined_model_filters)
+
+        return stmt
+
     async def create(self, db: AsyncSession, object: CreateSchemaType) -> ModelType:
         """
         Create a new record in the database.
@@ -585,7 +626,7 @@ class FastCRUD(
         self,
         db: AsyncSession,
         offset: int = 0,
-        limit: Optional[int] = 100,
+        limit: int = 100,
         schema_to_select: Optional[type[BaseModel]] = None,
         sort_columns: Optional[Union[str, list[str]]] = None,
         sort_orders: Optional[Union[str, list[str]]] = None,
@@ -604,7 +645,7 @@ class FastCRUD(
         Args:
             db: The database session to use for the operation.
             offset: Starting index for records to fetch, useful for pagination.
-            limit: Maximum number of records to fetch in one call. Use `None` for "no limit", fetching all matching rows. Note that in order to use `limit=None`, you'll have to provide a custom endpoint to facilitate it, which you should only do if you really seriously want to allow the user to get all the data at once.
+            limit: Maximum number of records to fetch in one call.
             schema_to_select: Optional Pydantic schema for selecting specific columns. Required if `return_as_model` is True.
             sort_columns: Column names to sort the results by.
             sort_orders: Corresponding sort orders ('asc', 'desc') for each column in sort_columns.
@@ -649,7 +690,7 @@ class FastCRUD(
             users = await crud.get_multi(db, 0, 10, is_active=True, sort_columns=['username', 'email'], sort_orders=['asc', 'desc'])
             ```
         """
-        if (limit is not None and limit < 0) or offset < 0:
+        if limit < 0 or offset < 0:
             raise ValueError("Limit and offset must be non-negative.")
 
         stmt = await self.select(
@@ -659,11 +700,7 @@ class FastCRUD(
             **kwargs,
         )
 
-        if offset:
-            stmt = stmt.offset(offset)
-        if limit is not None:
-            stmt = stmt.limit(limit)
-
+        stmt = stmt.offset(offset).limit(limit)
         result = await db.execute(stmt)
         data = [dict(row) for row in result.mappings()]
 
@@ -910,31 +947,9 @@ class FastCRUD(
                 )
             )
 
-        for join in join_definitions:
-            model = join.alias or join.model
-            joined_model_filters = (
-                self._parse_filters(model=model, **join.filters)
-                if join.filters
-                else None
-            )
-
-            join_select = _extract_matching_columns_from_schema(
-                model=join.model,
-                schema=join.schema_to_select,
-                prefix=join.join_prefix,
-                alias=join.alias,
-            )
-
-            if join.join_type == "left":
-                stmt = stmt.outerjoin(model, join.join_on).add_columns(*join_select)
-            elif join.join_type == "inner":
-                stmt = stmt.join(model, join.join_on).add_columns(*join_select)
-            else:
-                raise ValueError(f"Unsupported join type: {join.join_type}.")
-
-            if joined_model_filters is not None:
-                stmt = stmt.filter(*joined_model_filters)
-
+        stmt = self._prepare_and_apply_joins(
+            stmt=stmt, joins_config=join_definitions, use_temporary_prefix=nest_joins
+        )
         primary_filters = self._parse_filters(**kwargs)
         if primary_filters:
             stmt = stmt.filter(*primary_filters)
@@ -963,7 +978,7 @@ class FastCRUD(
         join_filters: Optional[dict] = None,
         nest_joins: bool = False,
         offset: int = 0,
-        limit: Optional[int] = 100,
+        limit: int = 100,
         sort_columns: Optional[Union[str, list[str]]] = None,
         sort_orders: Optional[Union[str, list[str]]] = None,
         return_as_model: bool = False,
@@ -992,7 +1007,7 @@ class FastCRUD(
             join_filters: Filters applied to the joined model, specified as a dictionary mapping column names to their expected values.
             nest_joins: If True, nested data structures will be returned where joined model data are nested under the join_prefix as a dictionary.
             offset: The offset (number of records to skip) for pagination.
-            limit: Maximum number of records to fetch in one call. Use `None` for "no limit", fetching all matching rows. Note that in order to use `limit=None`, you'll have to provide a custom endpoint to facilitate it, which you should only do if you really seriously want to allow the user to get all the data at once.
+            limit: The limit (maximum number of records to return) for pagination.
             sort_columns: A single column name or a list of column names on which to apply sorting.
             sort_orders: A single sort order ('asc' or 'desc') or a list of sort orders corresponding to the columns in sort_columns. If not provided, defaults to 'asc' for each column.
             return_as_model: If True, converts the fetched data to Pydantic models based on schema_to_select. Defaults to False.
@@ -1209,7 +1224,7 @@ class FastCRUD(
         elif not joins_config and not join_model:
             raise ValueError("You need one of join_model or joins_config.")
 
-        if (limit is not None and limit < 0) or offset < 0:
+        if limit < 0 or offset < 0:
             raise ValueError("Limit and offset must be non-negative.")
 
         primary_select = _extract_matching_columns_from_schema(
@@ -1232,30 +1247,9 @@ class FastCRUD(
                 )
             )
 
-        for join in join_definitions:
-            model = join.alias or join.model
-            joined_model_filters = (
-                self._parse_filters(model=model, **join.filters)
-                if join.filters
-                else None
-            )
-
-            join_select = _extract_matching_columns_from_schema(
-                model=join.model,
-                schema=join.schema_to_select,
-                prefix=join.join_prefix,
-                alias=join.alias,
-            )
-
-            if join.join_type == "left":
-                stmt = stmt.outerjoin(model, join.join_on).add_columns(*join_select)
-            elif join.join_type == "inner":
-                stmt = stmt.join(model, join.join_on).add_columns(*join_select)
-            else:
-                raise ValueError(f"Unsupported join type: {join.join_type}.")
-
-            if joined_model_filters is not None:
-                stmt = stmt.filter(*joined_model_filters)
+        stmt = self._prepare_and_apply_joins(
+            stmt=stmt, joins_config=join_definitions, use_temporary_prefix=nest_joins
+        )
 
         primary_filters = self._parse_filters(**kwargs)
         if primary_filters:
@@ -1264,10 +1258,7 @@ class FastCRUD(
         if sort_columns:
             stmt = self._apply_sorting(stmt, sort_columns, sort_orders)
 
-        if offset:
-            stmt = stmt.offset(offset)
-        if limit is not None:
-            stmt = stmt.limit(limit)
+        stmt = stmt.offset(offset).limit(limit)
 
         result = await db.execute(stmt)
         data: list[Union[dict, BaseModel]] = []
diff --git a/fastcrud/crud/helper.py b/fastcrud/crud/helper.py
index ad25fbf..19cfae6 100644
--- a/fastcrud/crud/helper.py
+++ b/fastcrud/crud/helper.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, NamedTuple
+from typing import Any, Optional, NamedTuple, Union
 
 from sqlalchemy import inspect
 from sqlalchemy.orm import DeclarativeBase
@@ -18,10 +18,12 @@ class JoinConfig(NamedTuple):
 
 
 def _extract_matching_columns_from_schema(
-    model: type[DeclarativeBase],
+    model: Union[type[DeclarativeBase], AliasedClass],
     schema: Optional[type[BaseModel]],
     prefix: Optional[str] = None,
     alias: Optional[AliasedClass] = None,
+    use_temporary_prefix: Optional[bool] = False,
+    temp_prefix: Optional[str] = "joined__",
 ) -> list[Any]:
     """
     Retrieves a list of ORM column objects from a SQLAlchemy model that match the field names in a given Pydantic schema,
@@ -33,6 +35,8 @@ def _extract_matching_columns_from_schema(
         schema: Optional; a Pydantic schema containing field names to be matched with the model's columns. If None, all columns from the model are used.
         prefix: Optional; a prefix to be added to all column names. If None, no prefix is added.
         alias: Optional; an alias for the model, used for referencing the columns through this alias in the query. If None, the original model is used.
+        use_temporary_prefix: Whether to use or not an aditional prefix for joins. Default False.
+        temp_prefix: The temporary prefix to be used. Default "joined__".
 
     Returns:
         A list of ORM column objects (potentially labeled with a prefix) that correspond to the field names defined
@@ -41,18 +45,31 @@ def _extract_matching_columns_from_schema(
     """
     model_or_alias = alias if alias else model
     columns = []
+    temp_prefix = (
+        temp_prefix if use_temporary_prefix and temp_prefix is not None else ""
+    )
     if schema:
         for field in schema.model_fields.keys():
             if hasattr(model_or_alias, field):
                 column = getattr(model_or_alias, field)
-                if prefix:
-                    column = column.label(f"{prefix}{field}")
+                if prefix is not None or use_temporary_prefix:
+                    column_label = (
+                        f"{temp_prefix}{prefix}{field}"
+                        if prefix
+                        else f"{temp_prefix}{field}"
+                    )
+                    column = column.label(column_label)
                 columns.append(column)
     else:
         for column in model.__table__.c:
             column = getattr(model_or_alias, column.key)
-            if prefix:
-                column = column.label(f"{prefix}{column.key}")
+            if prefix is not None or use_temporary_prefix:
+                column_label = (
+                    f"{temp_prefix}{prefix}{column.key}"
+                    if prefix
+                    else f"{temp_prefix}{column.key}"
+                )
+                column = column.label(column_label)
             columns.append(column)
 
     return columns
@@ -102,20 +119,33 @@ def _auto_detect_join_condition(
 
 
 def _nest_join_data(
-    data: dict[str, Any], join_definitions: list[JoinConfig]
+    data: dict[str, Any],
+    join_definitions: list[JoinConfig],
+    temp_prefix: str = "joined__",
 ) -> dict[str, Any]:
     nested_data: dict = {}
     for key, value in data.items():
         nested = False
         for join in join_definitions:
-            if join.join_prefix and key.startswith(join.join_prefix):
-                nested_key = join.join_prefix.rstrip("_")
-                nested_field = key[len(join.join_prefix) :]
+            full_prefix = (
+                f"{temp_prefix}{join.join_prefix}" if join.join_prefix else temp_prefix
+            )
+            if key.startswith(full_prefix):
+                nested_key = (
+                    join.join_prefix.rstrip("_")
+                    if join.join_prefix
+                    else join.model.__tablename__
+                )
+                nested_field = key[len(full_prefix) :]
                 if nested_key not in nested_data:
                     nested_data[nested_key] = {}
                 nested_data[nested_key][nested_field] = value
                 nested = True
                 break
         if not nested:
-            nested_data[key] = value
+            stripped_key = (
+                key[len(temp_prefix) :] if key.startswith(temp_prefix) else key
+            )
+            nested_data[stripped_key] = value
+
     return nested_data
diff --git a/fastcrud/endpoint/helper.py b/fastcrud/endpoint/helper.py
index b327371..d7c5ca9 100644
--- a/fastcrud/endpoint/helper.py
+++ b/fastcrud/endpoint/helper.py
@@ -45,23 +45,26 @@ class CRUDMethods(BaseModel):
 def _get_primary_key(
     model: type[DeclarativeBase],
 ) -> Union[str, None]:  # pragma: no cover
-    return _get_primary_keys(model)[0].name
+    key: Optional[str] = _get_primary_keys(model)[0].name
+    return key
 
 
 def _get_primary_keys(model: type[DeclarativeBase]) -> Sequence[Column]:
     """Get the primary key of a SQLAlchemy model."""
     inspector = inspect(model).mapper
-    primary_key_columns = inspector.primary_key
+    primary_key_columns: Sequence[Column] = inspector.primary_key
 
     return primary_key_columns
 
 
 def _get_python_type(column: Column) -> Optional[type]:
     try:
-        return column.type.python_type
+        direct_type: Optional[type] = column.type.python_type
+        return direct_type
     except NotImplementedError:
-        if hasattr(column.type, "impl") and hasattr(column.type.impl, "python_type"):  # type: ignore
-            return column.type.impl.python_type  # type: ignore
+        if hasattr(column.type, "impl") and hasattr(column.type.impl, "python_type"):
+            indirect_type: Optional[type] = column.type.impl.python_type
+            return indirect_type
         else:  # pragma: no cover
             raise NotImplementedError(
                 f"The primary key column {column.name} uses a custom type without a defined `python_type` or suitable `impl` fallback."
diff --git a/mypy.ini b/mypy.ini
index a1a2bc0..8c21fb7 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -3,6 +3,7 @@ python_version = 3.11
 warn_return_any = True
 warn_unused_configs = True
 ignore_missing_imports = True
+plugins = sqlalchemy.ext.mypy.plugin
 
 [mypy-src.app.*]
 disallow_untyped_defs = True
\ No newline at end of file


Test patch:
diff --git a/tests/sqlalchemy/crud/test_get_joined.py b/tests/sqlalchemy/crud/test_get_joined.py
index cb3590b..7e67335 100644
--- a/tests/sqlalchemy/crud/test_get_joined.py
+++ b/tests/sqlalchemy/crud/test_get_joined.py
@@ -465,3 +465,62 @@ async def test_get_joined_nest_joins(async_session, test_data, test_data_tier):
     assert (
         "tier_name" not in result
     ), "'tier_name' should not be at the top level in the result."
+
+
+@pytest.mark.asyncio
+async def test_get_joined_nested_no_prefix_provided(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+    result = await crud.get_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+        nest_joins=True,
+    )
+
+    assert result is not None, "No result returned, expected a nested join result."
+    assert "name" in result, "Expected primary model field 'name' in result."
+    assert (
+        "name" in result[TierModel.__tablename__]
+    ), f"Expected 'name' field inside nested '{TierModel.__tablename__}' dictionary."
+
+
+@pytest.mark.asyncio
+async def test_get_joined_no_prefix_no_nesting(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+
+    result = await crud.get_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+    )
+
+    assert result is not None, "Expected to retrieve a result from joined query."
+    assert (
+        "name" in result
+    ), "Expected 'name' field from the primary model in the result."
+    assert "tier_id" in result, "Expected 'tier_id' (foreign key) in the result."
+    assert (
+        "tier_name" not in result
+    ), "Field 'tier_name' should not exist unless specifically prefixed or nested."
diff --git a/tests/sqlalchemy/crud/test_get_multi.py b/tests/sqlalchemy/crud/test_get_multi.py
index 9b5a837..4334763 100644
--- a/tests/sqlalchemy/crud/test_get_multi.py
+++ b/tests/sqlalchemy/crud/test_get_multi.py
@@ -48,24 +48,6 @@ async def test_get_multi_pagination(async_session, test_model, test_data):
     assert len(result_page_2["data"]) == min(5, max(0, total_count - 5))
 
 
-@pytest.mark.asyncio
-async def test_get_multi_unpaginated(async_session, test_model, test_data):
-    for item in test_data:
-        record = test_model(**item)
-        async_session.add(record)
-    await async_session.commit()
-
-    total_count_query = await async_session.execute(
-        select(func.count()).select_from(test_model)
-    )
-    total_count = total_count_query.scalar()
-
-    crud = FastCRUD(test_model)
-    results = await crud.get_multi(async_session, offset=0, limit=None)
-
-    assert len(results["data"]) == total_count
-
-
 @pytest.mark.asyncio
 async def test_get_multi_sorting(async_session, test_model, test_data):
     for item in test_data:
diff --git a/tests/sqlalchemy/crud/test_get_multi_joined.py b/tests/sqlalchemy/crud/test_get_multi_joined.py
index 3dfe729..d26b57a 100644
--- a/tests/sqlalchemy/crud/test_get_multi_joined.py
+++ b/tests/sqlalchemy/crud/test_get_multi_joined.py
@@ -55,32 +55,6 @@ async def test_get_multi_joined_basic(async_session, test_data, test_data_tier):
     assert all("tier_name" in item for item in result["data"])
 
 
-@pytest.mark.asyncio
-async def test_get_multi_joined_unpaginated(async_session, test_data, test_data_tier):
-    for tier_item in test_data_tier:
-        async_session.add(TierModel(**tier_item))
-    await async_session.commit()
-
-    for user_item in test_data:
-        async_session.add(ModelTest(**user_item))
-    await async_session.commit()
-
-    crud = FastCRUD(ModelTest)
-    result = await crud.get_multi_joined(
-        db=async_session,
-        join_model=TierModel,
-        join_prefix="tier_",
-        schema_to_select=CreateSchemaTest,
-        join_schema_to_select=TierSchemaTest,
-        offset=0,
-        limit=None,
-    )
-
-    assert len(result["data"]) == len(test_data)
-    assert result["total_count"] == len(test_data)
-    assert all("tier_name" in item for item in result["data"])
-
-
 @pytest.mark.asyncio
 async def test_get_multi_joined_sorting(async_session, test_data, test_data_tier):
     for tier_item in test_data_tier:
@@ -756,3 +730,66 @@ async def test_get_multi_joined_with_nesting(async_session, test_data, test_data
             assert (
                 "category_" not in item["category"]
             ), "No prefix should be present in the nested category keys"
+
+
+@pytest.mark.asyncio
+async def test_get_multi_joined_no_prefix_regular(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+
+    result = await crud.get_multi_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+        limit=10,
+    )
+
+    print(result)
+    assert result and result["data"], "Expected data in the result."
+    for item in result["data"]:
+        assert "name" in item, "Expected user name in each item."
+        assert "name_1" in item, "Expected tier name in each item without prefix."
+
+
+@pytest.mark.asyncio
+async def test_get_multi_joined_no_prefix_nested(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+
+    result = await crud.get_multi_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+        nest_joins=True,
+        limit=10,
+    )
+
+    assert result and result["data"], "Expected data in the result."
+    for item in result["data"]:
+        assert "name" in item, "Expected user name in each item."
+        assert (
+            TierModel.__tablename__ in item
+        ), f"Expected nested '{TierModel.__tablename__}' key in each item."
+        assert (
+            "name" in item[TierModel.__tablename__]
+        ), f"Expected 'name' field inside nested '{TierModel.__tablename__}' dictionary."
diff --git a/tests/sqlmodel/crud/test_get_joined.py b/tests/sqlmodel/crud/test_get_joined.py
index 6385f2b..5bab616 100644
--- a/tests/sqlmodel/crud/test_get_joined.py
+++ b/tests/sqlmodel/crud/test_get_joined.py
@@ -465,3 +465,63 @@ async def test_get_joined_nest_joins(async_session, test_data, test_data_tier):
     assert (
         "tier_name" not in result
     ), "'tier_name' should not be at the top level in the result."
+
+
+@pytest.mark.asyncio
+async def test_get_joined_nested_no_prefix_provided(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+    result = await crud.get_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+        join_prefix="tier_",
+        nest_joins=True,
+    )
+
+    assert result is not None, "No result returned, expected a nested join result."
+    assert "name" in result, "Expected primary model field 'name' in result."
+    assert (
+        "name" in result["tier"]
+    ), "Expected 'name' field inside nested 'tier' dictionary."
+
+
+@pytest.mark.asyncio
+async def test_get_joined_no_prefix_no_nesting(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+
+    result = await crud.get_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+    )
+
+    assert result is not None, "Expected to retrieve a result from joined query."
+    assert (
+        "name" in result
+    ), "Expected 'name' field from the primary model in the result."
+    assert "tier_id" in result, "Expected 'tier_id' (foreign key) in the result."
+    assert (
+        "tier_name" not in result
+    ), "Field 'tier_name' should not exist unless specifically prefixed or nested."
diff --git a/tests/sqlmodel/crud/test_get_multi.py b/tests/sqlmodel/crud/test_get_multi.py
index 9b5a837..4334763 100644
--- a/tests/sqlmodel/crud/test_get_multi.py
+++ b/tests/sqlmodel/crud/test_get_multi.py
@@ -48,24 +48,6 @@ async def test_get_multi_pagination(async_session, test_model, test_data):
     assert len(result_page_2["data"]) == min(5, max(0, total_count - 5))
 
 
-@pytest.mark.asyncio
-async def test_get_multi_unpaginated(async_session, test_model, test_data):
-    for item in test_data:
-        record = test_model(**item)
-        async_session.add(record)
-    await async_session.commit()
-
-    total_count_query = await async_session.execute(
-        select(func.count()).select_from(test_model)
-    )
-    total_count = total_count_query.scalar()
-
-    crud = FastCRUD(test_model)
-    results = await crud.get_multi(async_session, offset=0, limit=None)
-
-    assert len(results["data"]) == total_count
-
-
 @pytest.mark.asyncio
 async def test_get_multi_sorting(async_session, test_model, test_data):
     for item in test_data:
diff --git a/tests/sqlmodel/crud/test_get_multi_joined.py b/tests/sqlmodel/crud/test_get_multi_joined.py
index d480721..a80df17 100644
--- a/tests/sqlmodel/crud/test_get_multi_joined.py
+++ b/tests/sqlmodel/crud/test_get_multi_joined.py
@@ -55,32 +55,6 @@ async def test_get_multi_joined_basic(async_session, test_data, test_data_tier):
     assert all("tier_name" in item for item in result["data"])
 
 
-@pytest.mark.asyncio
-async def test_get_multi_joined_unpaginated(async_session, test_data, test_data_tier):
-    for tier_item in test_data_tier:
-        async_session.add(TierModel(**tier_item))
-    await async_session.commit()
-
-    for user_item in test_data:
-        async_session.add(ModelTest(**user_item))
-    await async_session.commit()
-
-    crud = FastCRUD(ModelTest)
-    result = await crud.get_multi_joined(
-        db=async_session,
-        join_model=TierModel,
-        join_prefix="tier_",
-        schema_to_select=CreateSchemaTest,
-        join_schema_to_select=TierSchemaTest,
-        offset=0,
-        limit=None,
-    )
-
-    assert len(result["data"]) == len(test_data)
-    assert result["total_count"] == len(test_data)
-    assert all("tier_name" in item for item in result["data"])
-
-
 @pytest.mark.asyncio
 async def test_get_multi_joined_sorting(async_session, test_data, test_data_tier):
     for tier_item in test_data_tier:
@@ -756,3 +730,66 @@ async def test_get_multi_joined_with_nesting(async_session, test_data, test_data
             assert (
                 "category_" not in item["category"]
             ), "No prefix should be present in the nested category keys"
+
+
+@pytest.mark.asyncio
+async def test_get_multi_joined_no_prefix_regular(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+
+    result = await crud.get_multi_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+        limit=10,
+    )
+
+    print(result)
+    assert result and result["data"], "Expected data in the result."
+    for item in result["data"]:
+        assert "name" in item, "Expected user name in each item."
+        assert "name_1" in item, "Expected tier name in each item without prefix."
+
+
+@pytest.mark.asyncio
+async def test_get_multi_joined_no_prefix_nested(
+    async_session, test_data, test_data_tier
+):
+    for tier_item in test_data_tier:
+        async_session.add(TierModel(**tier_item))
+    await async_session.commit()
+
+    for user_item in test_data:
+        async_session.add(ModelTest(**user_item))
+    await async_session.commit()
+
+    crud = FastCRUD(ModelTest)
+
+    result = await crud.get_multi_joined(
+        db=async_session,
+        join_model=TierModel,
+        schema_to_select=CreateSchemaTest,
+        join_schema_to_select=TierSchemaTest,
+        nest_joins=True,
+        limit=10,
+    )
+
+    assert result and result["data"], "Expected data in the result."
+    for item in result["data"]:
+        assert "name" in item, "Expected user name in each item."
+        assert (
+            TierModel.__tablename__ in item
+        ), f"Expected nested '{TierModel.__tablename__}' key in each item."
+        assert (
+            "name" in item[TierModel.__tablename__]
+        ), f"Expected 'name' field inside nested '{TierModel.__tablename__}' dictionary."