Repository: igorbenav/fastcrud
Pull request #105 (2024-06-19 16:02:01)

Simplified endpoints
# Pull Request Template for FastCRUD

## Description
This PR implements https://github.com/igorbenav/fastcrud/issues/67. It covers 2 issues:

1. It provides support for passing empty endpoint_names to fastcrud.crud_router() without redundant `//` in generated paths
```
endpoint_names = {
    "create": "",
    "read": "",
    "update": "",
    "delete": "",
    "db_delete": "",
    "read_multi": "",
    "read_paginated": "get_paginated",
}
```

2. It mixes existing _read_paginated endpoint logic into _read_items. Now "page" and "items_per_page" query parameters can be passed to _read_paginated resulting in same behavour. I also confirmed that old-style request for _read_items persist it's functinality. Default:


```curl -X 'GET' \
  'http://localhost:8000/users/get_multi?offset=0&limit=100' \
  -H 'accept: application/json'
```

works for both and new _read_items.



3. Appropriate warning have been added as needed.

4. I've added small update to advanced filters AND clauses documentation.


## Tests
Describe the tests you added or modified to cover your changes, if applicable.

## Checklist
- [x] I have read the [CONTRIBUTING](CONTRIBUTING.md) document.
- [x] My code follows the code style of this project.
- [x] I have added necessary documentation (if appropriate).
- [x] I have added tests that cover my changes (if applicable).
- [x] All new and existing tests passed.

## Additional Notes
Include any additional information that you think is important for reviewers to know.


JakNowy: @igorbenav are you ok with this direction? Ultimately I would like to make the plain endpoints the default behaviour, as well as mix read_paginated with read_items and drop read_paginated. But those changes are breaking, so the question arises are you planning any major breaking release soon or should I just keep the changes minimalistic for now?
igorbenav: If we really use the simplified as the default, the changes are breaking in nature. I believe the best approach for the first version of this is:
- making it possible to pass empty endpoint_names to fastcrud.crud_router() 
- warning that the simplified will be the standard for the next version when someone uses it without specifying `endpoint_names`
- warning that in the next version `_read_paginated` and `_read_items` will be unified whenever people create a router with `_read_paginated` in it
igorbenav: Then, after this, we may merge a fully breaking version without the warnings. Let's say 0.14.x has this warnings, 0.15.0 implements the breaking changes

Patch:
diff --git a/docs/advanced/crud.md b/docs/advanced/crud.md
index 8d94eab..4dea3df 100644
--- a/docs/advanced/crud.md
+++ b/docs/advanced/crud.md
@@ -135,7 +135,7 @@ items = await item_crud.get_multi(
 - __in - included in
 - __not_in - not included in
 
-### OR parameter filters
+### OR clauses
 
 More complex OR filters are supported. They must be passed as dictionary, where each key is a library-supported operator to be used in OR expression and values is what get's passed as the parameter.
 
@@ -147,6 +147,16 @@ items = await item_crud.get_multi(
 )
 ```
 
+### AND clauses
+And clauses can be achieved by chaining multiple filters together.
+
+# Fetch items priced under $20 and over 2 years of warranty.
+items = await item_crud.get_multi(
+    db=db,
+    price__lt=20,
+    warranty_years__gt=2,
+)
+
 
 #### Counting Records
 
diff --git a/docs/advanced/endpoint.md b/docs/advanced/endpoint.md
index d007402..68b2020 100644
--- a/docs/advanced/endpoint.md
+++ b/docs/advanced/endpoint.md
@@ -73,6 +73,17 @@ FastCRUD automates the creation of CRUD (Create, Read, Update, Delete) endpoints
   "items_per_page": 3
 }
 ```
+!!! WARNING
+
+        _read_paginated endpoint is getting deprecated and mixed into _read_items in the next major release.
+        Please use _read_items with optional page and items_per_pagequery params instead, to achieve pagination as before.
+        Simple _read_items behaviour persists with no breaking changes.
+
+        Read items paginated:
+        curl -X 'GET'   'http://localhost:8000/users/get_multi?page=2&itemsPerPage=10'   -H 'accept: application/json'
+
+        Read items unpaginated:
+        url -X 'GET'   'http://localhost:8000/users/get_multi?offset=0&limit=100'   -H 'accept: application/json'
 
 ### Update
 
@@ -226,6 +237,12 @@ app.include_router(endpoint_creator.router)
 
     You only need to pass the names of the endpoints you want to change in the endpoint_names dict.
 
+!!! WARNING
+
+    default_endpoint_names for EndpointCreator are going to be changed to empty strings in the next major release.
+    See:  https://github.com/igorbenav/fastcrud/issues/67
+
+
 ## Extending EndpointCreator
 
 You can create a subclass of `EndpointCreator` and override or add new methods to define custom routes. Here's an example:
diff --git a/docs/changelog.md b/docs/changelog.md
index 3163cb4..a4e74b9 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,83 +5,6 @@
 The Changelog documents all notable changes made to FastCRUD. This includes new features, bug fixes, and improvements. It's organized by version and date, providing a clear history of the library's development.
 
 ___
-## [0.13.1] - Jun 22, 2024
-
-#### Added
-- More Advanced Filters by @JakNowy 🎉
-
-#### Fixed
-- Bug where objects with null primary key are returned with all fields set to None in nested joins #102 
-
-#### Detailed Changes
-___
-
-### Advanced Filters
-
-FastCRUD supports advanced filtering options, allowing you to query records using operators such as greater than (`__gt`), less than (`__lt`), and their inclusive counterparts (`__gte`, `__lte`). These filters can be used in any method that retrieves or operates on records, including `get`, `get_multi`, `exists`, `count`, `update`, and `delete`.
-
-#### Single parameter filters
-
-Most filter operators require a single string or integer value.
-
-```python
-# Fetch items priced above $5
-items = await item_crud.get_multi(
-    db=db,
-    price__gte=5,
-)
-```
-
-Currently supported single parameter filters are:
-- __gt - greater than
-- __lt - less than
-- __gte - greater than or equal to
-- __lte - less than or equal to
-- __ne - not equal
-- __is - used to test True, False, and None identity
-- __is_not - negation of "is"
-- __like - SQL "like" search for specific text pattern
-- __notlike - negation of "like"
-- __ilike - case insensitive "like"
-- __notilike - case insensitive "notlike"
-- __startswith - text starts with given string
-- __endswith - text ends with given string
-- __contains - text contains given string
-- __match - database-specific match expression
-
-#### Complex parameter filters
-
-Some operators require multiple values. They must be passed as a python tuple, list, or set.
-
-```python
-# Fetch items priced between $5 and $20
-items = await item_crud.get_multi(
-    db=db,
-    price__between=(5, 20),
-)
-```
-- __between - between 2 numeric values
-- __in - included in
-- __not_in - not included in
-
-#### OR parameter filters
-
-More complex OR filters are supported. They must be passed as a dictionary, where each key is a library-supported operator to be used in OR expression and values are what get's passed as the parameter.
-
-```python
-# Fetch items priced under $5 or above $20
-items = await item_crud.get_multi(
-    db=db,
-    price__or={'lt': 5, 'gt': 20},
-)
-```
-
-#### What's Changed
-- Missing sqlalchemy operators by [@JakNowy](https://github.com/JakNowy) in https://github.com/igorbenav/fastcrud/pull/85
-- Null primary key bug fixed in https://github.com/igorbenav/fastcrud/pull/107
-
-**Full Changelog**: https://github.com/igorbenav/fastcrud/compare/v0.13.0...v0.13.1
-
 ## [0.13.0] - May 28, 2024
 
 #### Added
diff --git a/fastcrud/crud/fast_crud.py b/fastcrud/crud/fast_crud.py
index 82c3e3d..da08c74 100644
--- a/fastcrud/crud/fast_crud.py
+++ b/fastcrud/crud/fast_crud.py
@@ -17,7 +17,6 @@ from .helper import (
     _auto_detect_join_condition,
     _nest_join_data,
     _nest_multi_join_data,
-    _handle_null_primary_key_multi_join,
     JoinConfig,
 )
 
@@ -180,7 +179,6 @@ class FastCRUD(
         # Now 'archived' and 'archived_at' will be used for soft delete operations.
         ```
     """
-
     _SUPPORTED_FILTERS = {
         "gt": lambda column: column.__gt__,
         "lt": lambda column: column.__lt__,
@@ -217,13 +215,13 @@ class FastCRUD(
         self._primary_keys = _get_primary_keys(self.model)
 
     def _get_sqlalchemy_filter(
-        self,
-        operator: str,
-        value: Any,
-    ) -> Optional[Callable[[str], Callable]]:
-        if operator in {"in", "not_in", "between"}:
+        self, operator: str, value: Any,
+    ) ->Optional[Callable[[str], Callable]]:
+        if operator in {'in', 'not_in', 'between'}:
             if not isinstance(value, (tuple, list, set)):
-                raise ValueError(f"<{operator}> filter must be tuple, list or set")
+                raise ValueError(
+                    f"<{operator}> filter must be tuple, list or set"
+                )
         return self._SUPPORTED_FILTERS.get(operator)
 
     def _parse_filters(
@@ -238,22 +236,20 @@ class FastCRUD(
                 column = getattr(model, field_name, None)
                 if column is None:
                     raise ValueError(f"Invalid filter column: {field_name}")
-                if op == "or":
+                if op == 'or':
                     or_filters = [
                         sqlalchemy_filter(column)(or_value)
                         for or_key, or_value in value.items()
-                        if (
-                            sqlalchemy_filter := self._get_sqlalchemy_filter(
-                                or_key, value
-                            )
-                        )
-                        is not None
+                        if (sqlalchemy_filter := self._get_sqlalchemy_filter(
+                            or_key, value)) is not None
                     ]
                     filters.append(or_(*or_filters))
                 else:
                     sqlalchemy_filter = self._get_sqlalchemy_filter(op, value)
                     if sqlalchemy_filter:
-                        filters.append(sqlalchemy_filter(column)(value))
+                        filters.append(
+                                sqlalchemy_filter(column)(value)
+                        )
             else:
                 column = getattr(model, key, None)
                 if column is not None:
@@ -1405,9 +1401,6 @@ class FastCRUD(
         if (limit is not None and limit < 0) or offset < 0:
             raise ValueError("Limit and offset must be non-negative.")
 
-        if relationship_type is None:
-            relationship_type = "one-to-one"
-
         primary_select = _extract_matching_columns_from_schema(
             model=self.model, schema=schema_to_select
         )
@@ -1492,7 +1485,7 @@ class FastCRUD(
                 },
             )
         else:
-            nested_data = _handle_null_primary_key_multi_join(data, join_definitions)
+            nested_data = data
 
         response: dict[str, Any] = {"data": nested_data}
 
diff --git a/fastcrud/crud/helper.py b/fastcrud/crud/helper.py
index d45e68e..25ea3e5 100644
--- a/fastcrud/crud/helper.py
+++ b/fastcrud/crud/helper.py
@@ -18,7 +18,7 @@ class JoinConfig(BaseModel):
     join_type: str = "left"
     alias: Optional[AliasedClass] = None
     filters: Optional[dict] = None
-    relationship_type: Optional[str] = "one-to-one"
+    relationship_type: Optional[str] = None
 
     model_config = ConfigDict(arbitrary_types_allowed=True)
 
@@ -170,7 +170,7 @@ def _handle_one_to_one(nested_data, nested_key, nested_field, value):
             }
         }
     """
-    if nested_key not in nested_data or not isinstance(nested_data[nested_key], dict):
+    if nested_key not in nested_data:
         nested_data[nested_key] = {}
     nested_data[nested_key][nested_field] = value
     return nested_data
@@ -383,13 +383,6 @@ def _nest_join_data(
                 ):
                     nested_data[nested_key] = []
 
-        if nested_key in nested_data and isinstance(nested_data[nested_key], dict):
-            if (
-                join_primary_key in nested_data[nested_key]
-                and nested_data[nested_key][join_primary_key] is None
-            ):
-                nested_data[nested_key] = None
-
     assert nested_data is not None, "Couldn't nest the data."
     return nested_data
 
@@ -493,10 +486,6 @@ def _nest_multi_join_data(
                         item[join_primary_key] is None for item in value
                     ):  # pragma: no cover
                         new_row[key] = []
-                    elif (
-                        isinstance(value, dict) and value[join_primary_key] is None
-                    ):  # pragma: no cover
-                        new_row[key] = None
 
                 pre_nested_data[primary_key_value] = new_row
             else:
@@ -527,33 +516,3 @@ def _nest_multi_join_data(
                 nested_data[i] = schema_to_select(**item)
 
     return nested_data
-
-
-def _handle_null_primary_key_multi_join(
-    data: list[Union[dict[str, Any], BaseModel]], join_definitions: list[JoinConfig]
-) -> list[Union[dict[str, Any], BaseModel]]:
-    for item in data:
-        item_dict = item if isinstance(item, dict) else item.model_dump()
-
-        for join in join_definitions:
-            join_prefix = join.join_prefix or ""
-            nested_key = (
-                join_prefix.rstrip("_") if join_prefix else join.model.__tablename__
-            )
-
-            if nested_key in item_dict and isinstance(item_dict[nested_key], dict):
-                join_primary_key = _get_primary_key(join.model)
-
-                primary_key = join_primary_key
-                if join_primary_key:
-                    if (
-                        primary_key in item_dict[nested_key]
-                        and item_dict[nested_key][primary_key] is None
-                    ):  # pragma: no cover
-                        item_dict[nested_key] = None
-
-        if isinstance(item, BaseModel):
-            for key, value in item_dict.items():
-                setattr(item, key, value)
-
-    return data
diff --git a/fastcrud/endpoint/endpoint_creator.py b/fastcrud/endpoint/endpoint_creator.py
index f31b82d..cef3ee9 100644
--- a/fastcrud/endpoint/endpoint_creator.py
+++ b/fastcrud/endpoint/endpoint_creator.py
@@ -1,3 +1,4 @@
+import warnings
 from typing import Type, TypeVar, Optional, Callable, Sequence, Union
 from enum import Enum
 
@@ -254,6 +255,15 @@ class EndpointCreator:
             "read_paginated": "get_paginated",
         }
         self.endpoint_names = {**self.default_endpoint_names, **(endpoint_names or {})}
+        if self.endpoint_names == self.default_endpoint_names:
+            warnings.warn(
+                "Old default_endpoint_names are getting deprecated. "
+                "Default values are going to be replaced by empty strings, "
+                "resulting in plain endpoint names. "
+                "For details see:"
+                " https://github.com/igorbenav/fastcrud/issues/67",
+                DeprecationWarning
+            )
         if filter_config:
             if isinstance(filter_config, dict):
                 filter_config = FilterConfig(**filter_config)
@@ -310,17 +320,40 @@ class EndpointCreator:
 
         async def endpoint(
             db: AsyncSession = Depends(self.session),
-            offset: int = Query(0),
-            limit: int = Query(100),
+            page: Optional[int] = Query(
+                None, alias="page", description="Page number"
+            ),
+            items_per_page: Optional[int] = Query(
+                None, alias="itemsPerPage", description="Number of items per page"
+            ),
             filters: dict = Depends(dynamic_filters),
         ):
-            return await self.crud.get_multi(db, offset=offset, limit=limit, **filters)
+            if not (page and items_per_page):
+                return await self.crud.get_multi(db, offset=0, limit=100,
+                                                 **filters)
+
+            offset = compute_offset(page=page, items_per_page=items_per_page)
+            crud_data = await self.crud.get_multi(
+                db, offset=offset, limit=items_per_page, **filters
+            )
+
+            return paginated_response(
+                crud_data=crud_data, page=page, items_per_page=items_per_page
+            )  # pragma: no cover
 
         return endpoint
 
     def _read_paginated(self):
         """Creates an endpoint for reading multiple items from the database with pagination."""
         dynamic_filters = _create_dynamic_filters(self.filter_config, self.column_types)
+        warnings.warn(
+            "_read_paginated endpoint is getting deprecated and mixed "
+            "into _read_items in the next major release. "
+            "Please use _read_items with optional page and items_per_page "
+            "query params instead, to achieve pagination as before."
+            "Simple _read_items behaviour persists with no breaking changes.",
+            DeprecationWarning
+        )
 
         async def endpoint(
             db: AsyncSession = Depends(self.session),
@@ -332,6 +365,10 @@ class EndpointCreator:
             ),
             filters: dict = Depends(dynamic_filters),
         ):
+            if not (page and items_per_page):
+                return await self.crud.get_multi(db, offset=0, limit=100,
+                                                 **filters)
+
             offset = compute_offset(page=page, items_per_page=items_per_page)
             crud_data = await self.crud.get_multi(
                 db, offset=offset, limit=items_per_page, **filters
@@ -384,11 +421,19 @@ class EndpointCreator:
 
         return endpoint
 
-    def _get_endpoint_name(self, operation: str) -> str:
-        """Get the endpoint name for a given CRUD operation, using defaults if not overridden by the user."""
-        return self.endpoint_names.get(
+    def _get_endpoint_path(self, operation: str):
+        endpoint_name = self.endpoint_names.get(
             operation, self.default_endpoint_names.get(operation, operation)
         )
+        path = f"{self.path}/{endpoint_name}" if endpoint_name else self.path
+
+        if operation in {'read', 'update', 'delete', 'db_delete'}:
+            _primary_keys_path_suffix = "/".join(
+                f"{{{n}}}" for n in self.primary_key_names
+            )
+            path = f'{path}/{_primary_keys_path_suffix}'
+
+        return path
 
     def add_routes_to_router(
         self,
@@ -493,12 +538,9 @@ class EndpointCreator:
         if self.delete_schema:
             delete_description = "Soft delete a"
 
-        _primary_keys_path_suffix = "/".join(f"{{{n}}}" for n in self.primary_key_names)
-
         if ("create" in included_methods) and ("create" not in deleted_methods):
-            endpoint_name = self._get_endpoint_name("create")
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}",
+                self._get_endpoint_path(operation='create'),
                 self._create_item(),
                 methods=["POST"],
                 include_in_schema=self.include_in_schema,
@@ -508,10 +550,8 @@ class EndpointCreator:
             )
 
         if ("read" in included_methods) and ("read" not in deleted_methods):
-            endpoint_name = self._get_endpoint_name("read")
-
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
+                self._get_endpoint_path(operation='read'),
                 self._read_item(),
                 methods=["GET"],
                 include_in_schema=self.include_in_schema,
@@ -521,9 +561,8 @@ class EndpointCreator:
             )
 
         if ("read_multi" in included_methods) and ("read_multi" not in deleted_methods):
-            endpoint_name = self._get_endpoint_name("read_multi")
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}",
+                self._get_endpoint_path(operation='read_multi'),
                 self._read_items(),
                 methods=["GET"],
                 include_in_schema=self.include_in_schema,
@@ -535,9 +574,8 @@ class EndpointCreator:
         if ("read_paginated" in included_methods) and (
             "read_paginated" not in deleted_methods
         ):
-            endpoint_name = self._get_endpoint_name("read_paginated")
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}",
+                self._get_endpoint_path(operation='read_paginated'),
                 self._read_paginated(),
                 methods=["GET"],
                 include_in_schema=self.include_in_schema,
@@ -547,9 +585,8 @@ class EndpointCreator:
             )
 
         if ("update" in included_methods) and ("update" not in deleted_methods):
-            endpoint_name = self._get_endpoint_name("update")
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
+                self._get_endpoint_path(operation='update'),
                 self._update_item(),
                 methods=["PATCH"],
                 include_in_schema=self.include_in_schema,
@@ -559,9 +596,9 @@ class EndpointCreator:
             )
 
         if ("delete" in included_methods) and ("delete" not in deleted_methods):
-            endpoint_name = self._get_endpoint_name("delete")
+            path = self._get_endpoint_path(operation='delete')
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
+                path,
                 self._delete_item(),
                 methods=["DELETE"],
                 include_in_schema=self.include_in_schema,
@@ -575,9 +612,8 @@ class EndpointCreator:
             and ("db_delete" not in deleted_methods)
             and self.delete_schema
         ):
-            endpoint_name = self._get_endpoint_name("db_delete")
             self.router.add_api_route(
-                f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
+                self._get_endpoint_path(operation='db_delete'),
                 self._db_delete(),
                 methods=["DELETE"],
                 include_in_schema=self.include_in_schema,
diff --git a/pyproject.toml b/pyproject.toml
index 3e73fb5..e92d4d1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "fastcrud"
-version = "0.13.1"
+version = "0.13.0"
 description = "FastCRUD is a Python package for FastAPI, offering robust async CRUD operations and flexible endpoint creation utilities."
 authors = ["Igor Benav <igor.magalhaes.r@gmail.com>"]
 license = "MIT"


Test patch:
diff --git a/tests/sqlalchemy/conftest.py b/tests/sqlalchemy/conftest.py
index b2a896c..2b0a03f 100644
--- a/tests/sqlalchemy/conftest.py
+++ b/tests/sqlalchemy/conftest.py
@@ -130,48 +130,6 @@ class Article(Base):
 Card.articles = relationship("Article", order_by=Article.id, back_populates="card")
 
 
-class Client(Base):
-    __tablename__ = "clients"
-    id = Column(Integer, primary_key=True)
-    name = Column(String, nullable=False)
-    contact = Column(String, nullable=False)
-    phone = Column(String, nullable=False)
-    email = Column(String, nullable=False)
-
-
-class Department(Base):
-    __tablename__ = "departments"
-    id = Column(Integer, primary_key=True)
-    name = Column(String, nullable=False)
-
-
-class User(Base):
-    __tablename__ = "users"
-    id = Column(Integer, primary_key=True)
-    name = Column(String, nullable=False)
-    username = Column(String, nullable=False, unique=True)
-    email = Column(String, nullable=False, unique=True)
-    phone = Column(String, nullable=True)
-    profile_image_url = Column(String, nullable=True)
-    department_id = Column(Integer, ForeignKey("departments.id"), nullable=True)
-    company_id = Column(Integer, ForeignKey("clients.id"), nullable=True)
-    department = relationship("Department", backref="users")
-    company = relationship("Client", backref="users")
-
-
-class Task(Base):
-    __tablename__ = "tasks"
-    id = Column(Integer, primary_key=True)
-    name = Column(String, nullable=False)
-    description = Column(String, nullable=True)
-    client_id = Column(Integer, ForeignKey("clients.id"), nullable=True)
-    department_id = Column(Integer, ForeignKey("departments.id"), nullable=True)
-    assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True)
-    client = relationship("Client", backref="tasks")
-    department = relationship("Department", backref="tasks")
-    assignee = relationship("User", backref="tasks")
-
-
 class CreateSchemaTest(BaseModel):
     model_config = ConfigDict(extra="forbid")
     name: str
@@ -236,42 +194,6 @@ class CardSchema(BaseModel):
     articles: Optional[list[ArticleSchema]] = []
 
 
-class DepartmentRead(BaseModel):
-    id: int
-    name: str
-
-
-class UserReadSub(BaseModel):
-    id: int
-    name: str
-    username: str
-    email: str
-    phone: Optional[str]
-    profile_image_url: str
-    department_id: Optional[int]
-    company_id: Optional[int]
-
-
-class ClientRead(BaseModel):
-    id: int
-    name: str
-    contact: str
-    phone: str
-    email: str
-
-
-class TaskReadSub(BaseModel):
-    id: int
-    name: str
-    description: Optional[str]
-
-
-class TaskRead(TaskReadSub):
-    department: Optional[DepartmentRead]
-    assignee: Optional[UserReadSub]
-    client: Optional[ClientRead]
-
-
 async_engine = create_async_engine(
     "sqlite+aiosqlite:///:memory:", echo=True, future=True
 )
diff --git a/tests/sqlalchemy/core/test_parse_filters.py b/tests/sqlalchemy/core/test_parse_filters.py
index 5320542..58aa813 100644
--- a/tests/sqlalchemy/core/test_parse_filters.py
+++ b/tests/sqlalchemy/core/test_parse_filters.py
@@ -25,7 +25,9 @@ async def test_parse_filters_multiple_conditions(test_model):
 async def test_parse_filters_or_condition(test_model):
     fast_crud = FastCRUD(test_model)
 
-    filters = fast_crud._parse_filters(name__or={"gt": 1, "lt": 5})
+    filters = fast_crud._parse_filters(
+        name__or={'gt': 1, 'lt': 5}
+    )
     assert len(filters) == 1
     assert str(filters[0]) == "test.name > :name_1 OR test.name < :name_2"
 
diff --git a/tests/sqlalchemy/crud/test_get_joined.py b/tests/sqlalchemy/crud/test_get_joined.py
index 864b4ea..5f40e93 100644
--- a/tests/sqlalchemy/crud/test_get_joined.py
+++ b/tests/sqlalchemy/crud/test_get_joined.py
@@ -13,14 +13,6 @@ from ...sqlalchemy.conftest import (
     ReadSchemaTest,
     Article,
     Card,
-    Client,
-    Department,
-    User,
-    Task,
-    ClientRead,
-    DepartmentRead,
-    UserReadSub,
-    TaskRead,
 )
 
 
@@ -573,143 +565,3 @@ async def test_get_joined_card_with_articles(async_session):
     assert all(
         "title" in article for article in result["articles"]
     ), "Each article should have a title."
-
-
-@pytest.mark.asyncio
-async def test_get_joined_nested_data_none_dict(async_session):
-    clients = [
-        Client(
-            name="Client A",
-            contact="Contact A",
-            phone="111-111-1111",
-            email="a@client.com",
-        ),
-        Client(
-            name="Client B",
-            contact="Contact B",
-            phone="222-222-2222",
-            email="b@client.com",
-        ),
-    ]
-    async_session.add_all(clients)
-    await async_session.flush()
-
-    departments = [
-        Department(name="Department A"),
-        Department(name="Department B"),
-    ]
-    async_session.add_all(departments)
-    await async_session.flush()
-
-    users = [
-        User(
-            name="User A",
-            username="usera",
-            email="usera@example.com",
-            phone="123-123-1234",
-        ),
-        User(
-            name="User B",
-            username="userb",
-            email="userb@example.com",
-            phone="234-234-2345",
-        ),
-    ]
-    async_session.add_all(users)
-    await async_session.flush()
-
-    tasks = [
-        Task(
-            name="Task 1",
-            description="Task 1 Description",
-            client_id=clients[0].id,
-            department_id=departments[0].id,
-            assignee_id=users[0].id,
-        ),
-        Task(
-            name="Task 2",
-            description="Task 2 Description",
-            client_id=clients[1].id,
-            department_id=departments[1].id,
-            assignee_id=users[1].id,
-        ),
-        Task(
-            name="Task 3",
-            description="Task 3 Description",
-            client_id=None,
-            department_id=None,
-            assignee_id=None,
-        ),
-    ]
-    async_session.add_all(tasks)
-    await async_session.commit()
-
-    task_crud = FastCRUD(Task)
-
-    joins_config = [
-        JoinConfig(
-            model=Client,
-            join_on=Task.client_id == Client.id,
-            join_prefix="client_",
-            schema_to_select=ClientRead,
-            join_type="left",
-        ),
-        JoinConfig(
-            model=Department,
-            join_on=Task.department_id == Department.id,
-            join_prefix="department_",
-            schema_to_select=DepartmentRead,
-            join_type="left",
-        ),
-        JoinConfig(
-            model=User,
-            join_on=Task.assignee_id == User.id,
-            join_prefix="assignee_",
-            schema_to_select=UserReadSub,
-            join_type="left",
-        ),
-    ]
-
-    task1_result = await task_crud.get_joined(
-        db=async_session,
-        id=tasks[0].id,
-        schema_to_select=TaskRead,
-        joins_config=joins_config,
-        nest_joins=True,
-    )
-
-    assert task1_result is not None, "No data returned from the database."
-    assert (
-        "client" in task1_result
-    ), "Nested client data should be present under key 'client'"
-    assert (
-        "department" in task1_result
-    ), "Nested department data should be present under key 'department'"
-    assert (
-        "assignee" in task1_result
-    ), "Nested assignee data should be present under key 'assignee'"
-    assert task1_result["client"] is not None, "Task 1 should have a client."
-    assert task1_result["department"] is not None, "Task 1 should have a department."
-    assert task1_result["assignee"] is not None, "Task 1 should have an assignee."
-
-    task3_result = await task_crud.get_joined(
-        db=async_session,
-        id=tasks[2].id,
-        schema_to_select=TaskRead,
-        joins_config=joins_config,
-        nest_joins=True,
-    )
-
-    assert task3_result is not None, "No data returned from the database."
-    assert (
-        "client" in task3_result
-    ), "Nested client data should be present under key 'client'"
-    assert (
-        "department" in task3_result
-    ), "Nested department data should be present under key 'department'"
-    assert (
-        "assignee" in task3_result
-    ), "Nested assignee data should be present under key 'assignee'"
-    assert task3_result["client"] is None, "Task 3 should have no client."
-    assert task3_result["department"] is None, "Task 3 should have no department."
-    assert task3_result["assignee"] is None, "Task 3 should have no assignee."
diff --git a/tests/sqlalchemy/crud/test_get_multi_joined.py b/tests/sqlalchemy/crud/test_get_multi_joined.py
index b8a02a6..a9ec4b9 100644
--- a/tests/sqlalchemy/crud/test_get_multi_joined.py
+++ b/tests/sqlalchemy/crud/test_get_multi_joined.py
@@ -19,14 +19,6 @@ from ...sqlalchemy.conftest import (
     Card,
     ArticleSchema,
     CardSchema,
-    Client,
-    Department,
-    User,
-    Task,
-    ClientRead,
-    DepartmentRead,
-    UserReadSub,
-    TaskRead,
 )
 
 
@@ -756,20 +748,18 @@ async def test_get_multi_joined_with_nesting(async_session, test_data, test_data
             assert (
                 "category" in item
             ), "Nested category data should be present under key 'category'"
-            assert item["tier"] is None or isinstance(
+            assert isinstance(
                 item["tier"], dict
             ), "Nested tier data should be a dictionary"
-            assert item["category"] is None or isinstance(
+            assert isinstance(
                 item["category"], dict
             ), "Nested category data should be a dictionary"
-            if item["tier"] is not None:
-                assert (
-                    "tier_" not in item["tier"]
-                ), "No prefix should be present in the nested tier keys"
-            if item["category"] is not None:  # pragma: no cover
-                assert (
-                    "category_" not in item["category"]
-                ), "No prefix should be present in the nested category keys"
+            assert (
+                "tier_" not in item["tier"]
+            ), "No prefix should be present in the nested tier keys"
+            assert (
+                "category_" not in item["category"]
+            ), "No prefix should be present in the nested category keys"
 
 
 @pytest.mark.asyncio
@@ -1084,118 +1074,3 @@ async def test_get_multi_joined_card_with_multiple_articles_as_models(async_sess
         card_d, "articles"
     ), "Card D should have nested articles."
     assert len(card_d.articles) == 0, "Card D should have no articles."
-
-
-@pytest.mark.asyncio
-async def test_get_multi_joined_nested_data_none_dict(async_session):
-    clients = [
-        Client(
-            name="Client A",
-            contact="Contact A",
-            phone="111-111-1111",
-            email="a@client.com",
-        ),
-        Client(
-            name="Client B",
-            contact="Contact B",
-            phone="222-222-2222",
-            email="b@client.com",
-        ),
-    ]
-    async_session.add_all(clients)
-    await async_session.flush()
-
-    departments = [
-        Department(name="Department A"),
-        Department(name="Department B"),
-    ]
-    async_session.add_all(departments)
-    await async_session.flush()
-
-    users = [
-        User(
-            name="User A",
-            username="usera",
-            email="usera@example.com",
-            phone="123-123-1234",
-        ),
-        User(
-            name="User B",
-            username="userb",
-            email="userb@example.com",
-            phone="234-234-2345",
-        ),
-    ]
-    async_session.add_all(users)
-    await async_session.flush()
-
-    tasks = [
-        Task(
-            name="Task 1",
-            description="Task 1 Description",
-            client_id=clients[0].id,
-            department_id=departments[0].id,
-            assignee_id=users[0].id,
-        ),
-        Task(
-            name="Task 2",
-            description="Task 2 Description",
-            client_id=clients[1].id,
-            department_id=departments[1].id,
-            assignee_id=users[1].id,
-        ),
-        Task(
-            name="Task 3",
-            description="Task 3 Description",
-            client_id=None,
-            department_id=None,
-            assignee_id=None,
-        ),
-    ]
-    async_session.add_all(tasks)
-    await async_session.commit()
-
-    task_crud = FastCRUD(Task)
-
-    result = await task_crud.get_multi_joined(
-        db=async_session,
-        nest_joins=True,
-        schema_to_select=TaskRead,
-        joins_config=[
-            JoinConfig(
-                model=Client,
-                join_on=Task.client_id == Client.id,
-                join_prefix="client",
-                schema_to_select=ClientRead,
-                join_type="left",
-            ),
-            JoinConfig(
-                model=Department,
-                join_on=Task.department_id == Department.id,
-                join_prefix="department",
-                schema_to_select=DepartmentRead,
-                join_type="left",
-            ),
-            JoinConfig(
-                model=User,
-                join_on=Task.assignee_id == User.id,
-                join_prefix="assignee",
-                schema_to_select=UserReadSub,
-                join_type="left",
-            ),
-        ],
-    )
-
-    assert result is not None, "No data returned from the database."
-    data = result["data"]
-    assert len(data) == 3, "Expected three tasks."
-
-    task1 = data[0]
-    assert task1["client"] is not None, "Task 1 should have a client."
-    assert task1["department"] is not None, "Task 1 should have a department."
-    assert task1["assignee"] is not None, "Task 1 should have an assignee."
-
-    task3 = data[2]
-    assert task3["client"] is None, "Task 3 should have no client."
-    assert task3["department"] is None, "Task 3 should have no department."
-    assert task3["assignee"] is None, "Task 3 should have no assignee."
diff --git a/tests/sqlalchemy/endpoint/test_endpoint_custom_names.py b/tests/sqlalchemy/endpoint/test_endpoint_custom_names.py
index b612e34..6fc5c6e 100644
--- a/tests/sqlalchemy/endpoint/test_endpoint_custom_names.py
+++ b/tests/sqlalchemy/endpoint/test_endpoint_custom_names.py
@@ -4,6 +4,17 @@ from fastapi.testclient import TestClient
 from fastcrud import crud_router
 
 
+@pytest.mark.parametrize('custom_endpoint_names, endpoint_paths', [
+    (
+            {"create": "", "read": "", "read_multi": ""},
+            ["/test_custom_names", "/test_custom_names",
+             "/test_custom_names"]
+    ),
+    (
+            {"create": "add", "read": "fetch", "read_multi": "fetch_multi"},
+            ["/test_custom_names/add", "/test_custom_names/fetch",
+             "/test_custom_names/fetch_multi"]),
+])
 @pytest.mark.asyncio
 async def test_endpoint_custom_names(
     client: TestClient,
@@ -12,16 +23,13 @@ async def test_endpoint_custom_names(
     test_model,
     create_schema,
     update_schema,
+    custom_endpoint_names,
+    endpoint_paths,
 ):
     for item in test_data:
         async_session.add(test_model(**item))
     await async_session.commit()
 
-    custom_endpoint_names = {
-        "create": "add",
-        "read": "fetch",
-    }
-
     custom_router = crud_router(
         session=lambda: async_session,
         model=test_model,
@@ -34,19 +42,31 @@ async def test_endpoint_custom_names(
 
     client.app.include_router(custom_router)
 
+    create_path, read_path, read_multi_path = endpoint_paths
+
     create_response = client.post(
-        "/test_custom_names/add", json={"name": "Custom Endpoint Item", "tier_id": 1}
+        create_path, json={"name": "Custom Endpoint Item", "tier_id": 1}
     )
+
     assert (
         create_response.status_code == 200
     ), "Failed to create item with custom endpoint name"
 
     item_id = create_response.json()["id"]
 
-    fetch_response = client.get(f"/test_custom_names/fetch/{item_id}")
+    fetch_response = client.get(f'{read_path}/{item_id}')
     assert (
         fetch_response.status_code == 200
     ), "Failed to fetch item with custom endpoint name"
     assert (
         fetch_response.json()["id"] == item_id
-    ), "Fetched item ID does not match created item ID"
+    ), (f"Fetched item ID does not match created item ID:"
+        f" {fetch_response.json()['id']} != {item_id}")
+
+    fetch_multi_response = client.get(read_multi_path)
+    assert (
+        fetch_multi_response.status_code == 200
+    ), "Failed to fetch multi items with custom endpoint name"
+    assert (
+        len(fetch_multi_response.json()['data']) == 12
+    ), "Fetched item list has incorrect length"
diff --git a/tests/sqlmodel/conftest.py b/tests/sqlmodel/conftest.py
index d4b2ef8..9d29a9d 100644
--- a/tests/sqlmodel/conftest.py
+++ b/tests/sqlmodel/conftest.py
@@ -105,53 +105,6 @@ class Article(SQLModel, table=True):
     card: Optional[Card] = Relationship(back_populates="articles")
 
 
-class Client(SQLModel, table=True):
-    __tablename__ = "clients"
-    id: Optional[int] = Field(default=None, primary_key=True)
-    name: str = Field(nullable=False)
-    contact: str = Field(nullable=False)
-    phone: str = Field(nullable=False)
-    email: str = Field(nullable=False)
-    tasks: list["Task"] = Relationship(back_populates="client")
-    users: list["User"] = Relationship(back_populates="company")
-
-
-class Department(SQLModel, table=True):
-    __tablename__ = "departments"
-    id: Optional[int] = Field(default=None, primary_key=True)
-    name: str = Field(nullable=False)
-    tasks: list["Task"] = Relationship(back_populates="department")
-    users: list["User"] = Relationship(back_populates="department")
-
-
-class User(SQLModel, table=True):
-    __tablename__ = "users"
-    id: Optional[int] = Field(default=None, primary_key=True)
-    name: str = Field(nullable=False)
-    username: str = Field(nullable=False, unique=True)
-    email: str = Field(nullable=False, unique=True)
-    phone: Optional[str] = Field(default=None)
-    profile_image_url: Optional[str] = Field(default=None)
-    department_id: Optional[int] = Field(default=None, foreign_key="departments.id")
-    company_id: Optional[int] = Field(default=None, foreign_key="clients.id")
-    department: Optional[Department] = Relationship(back_populates="users")
-    company: Optional[Client] = Relationship(back_populates="users")
-    tasks: list["Task"] = Relationship(back_populates="assignee")
-
-
-class Task(SQLModel, table=True):
-    __tablename__ = "tasks"
-    id: Optional[int] = Field(default=None, primary_key=True)
-    name: str = Field(nullable=False)
-    description: Optional[str] = Field(default=None)
-    client_id: Optional[int] = Field(default=None, foreign_key="clients.id")
-    department_id: Optional[int] = Field(default=None, foreign_key="departments.id")
-    assignee_id: Optional[int] = Field(default=None, foreign_key="users.id")
-    client: Optional[Client] = Relationship(back_populates="tasks")
-    department: Optional[Department] = Relationship(back_populates="tasks")
-    assignee: Optional[User] = Relationship(back_populates="tasks")
-
-
 class CreateSchemaTest(SQLModel):
     model_config = ConfigDict(extra="forbid")
     name: str
@@ -215,7 +168,7 @@ class MultiPkCreate(SQLModel):
 
 class MultiPkSchema(SQLModel):
     name: str
-    test_id: Optional[int] = None
+    test_id: int = None
 
 
 class ArticleSchema(SQLModel):
@@ -230,42 +183,6 @@ class CardSchema(SQLModel):
     articles: Optional[list[ArticleSchema]] = []
 
 
-class DepartmentRead(SQLModel):
-    id: int
-    name: str
-
-
-class UserReadSub(SQLModel):
-    id: int
-    name: str
-    username: str
-    email: str
-    phone: Optional[str]
-    profile_image_url: str
-    department_id: Optional[int]
-    company_id: Optional[int]
-
-
-class ClientRead(SQLModel):
-    id: int
-    name: str
-    contact: str
-    phone: str
-    email: str
-
-
-class TaskReadSub(SQLModel):
-    id: int
-    name: str
-    description: Optional[str]
-
-
-class TaskRead(TaskReadSub):
-    department: Optional[DepartmentRead]
-    assignee: Optional[UserReadSub]
-    client: Optional[ClientRead]
-
-
 async_engine = create_async_engine(
     "sqlite+aiosqlite:///:memory:", echo=True, future=True
 )
diff --git a/tests/sqlmodel/core/test_parse_filters.py b/tests/sqlmodel/core/test_parse_filters.py
index 5320542..e5cecb6 100644
--- a/tests/sqlmodel/core/test_parse_filters.py
+++ b/tests/sqlmodel/core/test_parse_filters.py
@@ -25,7 +25,9 @@ async def test_parse_filters_multiple_conditions(test_model):
 async def test_parse_filters_or_condition(test_model):
     fast_crud = FastCRUD(test_model)
 
-    filters = fast_crud._parse_filters(name__or={"gt": 1, "lt": 5})
+    filters = fast_crud._parse_filters(
+        name__or={'gt': 1, 'lt': 5}
+    )
     assert len(filters) == 1
     assert str(filters[0]) == "test.name > :name_1 OR test.name < :name_2"
 
@@ -38,16 +40,6 @@ async def test_parse_filters_contained_in(test_model):
     assert str(filters[0]) == "test.category_id IN (__[POSTCOMPILE_category_id_1])"
 
 
-@pytest.mark.asyncio
-async def test_parse_filters_not_contained_in(test_model):
-    fast_crud = FastCRUD(test_model)
-    filters = fast_crud._parse_filters(category_id__not_in=[1, 2])
-    assert len(filters) == 1
-    assert (
-        str(filters[0]) == "(test.category_id NOT IN (__[POSTCOMPILE_category_id_1]))"
-    )
-
-
 @pytest.mark.asyncio
 @pytest.mark.parametrize("operator", ("in", "not_in", "between"))
 async def test_parse_filters_raises_exception(test_model, operator: str):
diff --git a/tests/sqlmodel/crud/test_get_joined.py b/tests/sqlmodel/crud/test_get_joined.py
index 3faa06b..2d63490 100644
--- a/tests/sqlmodel/crud/test_get_joined.py
+++ b/tests/sqlmodel/crud/test_get_joined.py
@@ -13,14 +13,6 @@ from ...sqlmodel.conftest import (
     ReadSchemaTest,
     Article,
     Card,
-    Client,
-    Department,
-    User,
-    Task,
-    ClientRead,
-    DepartmentRead,
-    UserReadSub,
-    TaskRead,
 )
 
 
@@ -574,143 +566,3 @@ async def test_get_joined_card_with_articles(async_session):
     assert all(
         "title" in article for article in result["articles"]
     ), "Each article should have a title."
-
-
-@pytest.mark.asyncio
-async def test_get_joined_nested_data_none_dict(async_session):
-    clients = [
-        Client(
-            name="Client A",
-            contact="Contact A",
-            phone="111-111-1111",
-            email="a@client.com",
-        ),
-        Client(
-            name="Client B",
-            contact="Contact B",
-            phone="222-222-2222",
-            email="b@client.com",
-        ),
-    ]
-    async_session.add_all(clients)
-    await async_session.flush()
-
-    departments = [
-        Department(name="Department A"),
-        Department(name="Department B"),
-    ]
-    async_session.add_all(departments)
-    await async_session.flush()
-
-    users = [
-        User(
-            name="User A",
-            username="usera",
-            email="usera@example.com",
-            phone="123-123-1234",
-        ),
-        User(
-            name="User B",
-            username="userb",
-            email="userb@example.com",
-            phone="234-234-2345",
-        ),
-    ]
-    async_session.add_all(users)
-    await async_session.flush()
-
-    tasks = [
-        Task(
-            name="Task 1",
-            description="Task 1 Description",
-            client_id=clients[0].id,
-            department_id=departments[0].id,
-            assignee_id=users[0].id,
-        ),
-        Task(
-            name="Task 2",
-            description="Task 2 Description",
-            client_id=clients[1].id,
-            department_id=departments[1].id,
-            assignee_id=users[1].id,
-        ),
-        Task(
-            name="Task 3",
-            description="Task 3 Description",
-            client_id=None,
-            department_id=None,
-            assignee_id=None,
-        ),
-    ]
-    async_session.add_all(tasks)
-    await async_session.commit()
-
-    task_crud = FastCRUD(Task)
-
-    joins_config = [
-        JoinConfig(
-            model=Client,
-            join_on=Task.client_id == Client.id,
-            join_prefix="client_",
-            schema_to_select=ClientRead,
-            join_type="left",
-        ),
-        JoinConfig(
-            model=Department,
-            join_on=Task.department_id == Department.id,
-            join_prefix="department_",
-            schema_to_select=DepartmentRead,
-            join_type="left",
-        ),
-        JoinConfig(
-            model=User,
-            join_on=Task.assignee_id == User.id,
-            join_prefix="assignee_",
-            schema_to_select=UserReadSub,
-            join_type="left",
-        ),
-    ]
-
-    task1_result = await task_crud.get_joined(
-        db=async_session,
-        id=tasks[0].id,
-        schema_to_select=TaskRead,
-        joins_config=joins_config,
-        nest_joins=True,
-    )
-
-    assert task1_result is not None, "No data returned from the database."
-    assert (
-        "client" in task1_result
-    ), "Nested client data should be present under key 'client'"
-    assert (
-        "department" in task1_result
-    ), "Nested department data should be present under key 'department'"
-    assert (
-        "assignee" in task1_result
-    ), "Nested assignee data should be present under key 'assignee'"
-    assert task1_result["client"] is not None, "Task 1 should have a client."
-    assert task1_result["department"] is not None, "Task 1 should have a department."
-    assert task1_result["assignee"] is not None, "Task 1 should have an assignee."
-
-    task3_result = await task_crud.get_joined(
-        db=async_session,
-        id=tasks[2].id,
-        schema_to_select=TaskRead,
-        joins_config=joins_config,
-        nest_joins=True,
-    )
-
-    assert task3_result is not None, "No data returned from the database."
-    assert (
-        "client" in task3_result
-    ), "Nested client data should be present under key 'client'"
-    assert (
-        "department" in task3_result
-    ), "Nested department data should be present under key 'department'"
-    assert (
-        "assignee" in task3_result
-    ), "Nested assignee data should be present under key 'assignee'"
-    assert task3_result["client"] is None, "Task 3 should have no client."
-    assert task3_result["department"] is None, "Task 3 should have no department."
-    assert task3_result["assignee"] is None, "Task 3 should have no assignee."
diff --git a/tests/sqlmodel/crud/test_get_multi_joined.py b/tests/sqlmodel/crud/test_get_multi_joined.py
index 4d91318..79e4fad 100644
--- a/tests/sqlmodel/crud/test_get_multi_joined.py
+++ b/tests/sqlmodel/crud/test_get_multi_joined.py
@@ -19,14 +19,6 @@ from ...sqlmodel.conftest import (
     Card,
     ArticleSchema,
     CardSchema,
-    Client,
-    Department,
-    User,
-    Task,
-    ClientRead,
-    DepartmentRead,
-    UserReadSub,
-    TaskRead,
 )
 
 
@@ -756,20 +748,18 @@ async def test_get_multi_joined_with_nesting(async_session, test_data, test_data
             assert (
                 "category" in item
             ), "Nested category data should be present under key 'category'"
-            assert item["tier"] is None or isinstance(
+            assert isinstance(
                 item["tier"], dict
             ), "Nested tier data should be a dictionary"
-            assert item["category"] is None or isinstance(
+            assert isinstance(
                 item["category"], dict
             ), "Nested category data should be a dictionary"
-            if item["tier"] is not None:
-                assert (
-                    "tier_" not in item["tier"]
-                ), "No prefix should be present in the nested tier keys"
-            if item["category"] is not None:  # pragma: no cover
-                assert (
-                    "category_" not in item["category"]
-                ), "No prefix should be present in the nested category keys"
+            assert (
+                "tier_" not in item["tier"]
+            ), "No prefix should be present in the nested tier keys"
+            assert (
+                "category_" not in item["category"]
+            ), "No prefix should be present in the nested category keys"
 
 
 @pytest.mark.asyncio
@@ -1084,118 +1074,3 @@ async def test_get_multi_joined_card_with_multiple_articles_as_models(async_sess
         card_d, "articles"
     ), "Card D should have nested articles."
     assert len(card_d.articles) == 0, "Card D should have no articles."
-
-
-@pytest.mark.asyncio
-async def test_get_multi_joined_nested_data_none_dict(async_session):
-    clients = [
-        Client(
-            name="Client A",
-            contact="Contact A",
-            phone="111-111-1111",
-            email="a@client.com",
-        ),
-        Client(
-            name="Client B",
-            contact="Contact B",
-            phone="222-222-2222",
-            email="b@client.com",
-        ),
-    ]
-    async_session.add_all(clients)
-    await async_session.flush()
-
-    departments = [
-        Department(name="Department A"),
-        Department(name="Department B"),
-    ]
-    async_session.add_all(departments)
-    await async_session.flush()
-
-    users = [
-        User(
-            name="User A",
-            username="usera",
-            email="usera@example.com",
-            phone="123-123-1234",
-        ),
-        User(
-            name="User B",
-            username="userb",
-            email="userb@example.com",
-            phone="234-234-2345",
-        ),
-    ]
-    async_session.add_all(users)
-    await async_session.flush()
-
-    tasks = [
-        Task(
-            name="Task 1",
-            description="Task 1 Description",
-            client_id=clients[0].id,
-            department_id=departments[0].id,
-            assignee_id=users[0].id,
-        ),
-        Task(
-            name="Task 2",
-            description="Task 2 Description",
-            client_id=clients[1].id,
-            department_id=departments[1].id,
-            assignee_id=users[1].id,
-        ),
-        Task(
-            name="Task 3",
-            description="Task 3 Description",
-            client_id=None,
-            department_id=None,
-            assignee_id=None,
-        ),
-    ]
-    async_session.add_all(tasks)
-    await async_session.commit()
-
-    task_crud = FastCRUD(Task)
-
-    result = await task_crud.get_multi_joined(
-        db=async_session,
-        nest_joins=True,
-        schema_to_select=TaskRead,
-        joins_config=[
-            JoinConfig(
-                model=Client,
-                join_on=Task.client_id == Client.id,
-                join_prefix="client",
-                schema_to_select=ClientRead,
-                join_type="left",
-            ),
-            JoinConfig(
-                model=Department,
-                join_on=Task.department_id == Department.id,
-                join_prefix="department",
-                schema_to_select=DepartmentRead,
-                join_type="left",
-            ),
-            JoinConfig(
-                model=User,
-                join_on=Task.assignee_id == User.id,
-                join_prefix="assignee",
-                schema_to_select=UserReadSub,
-                join_type="left",
-            ),
-        ],
-    )
-
-    assert result is not None, "No data returned from the database."
-    data = result["data"]
-    assert len(data) == 3, "Expected three tasks."
-
-    task1 = data[0]
-    assert task1["client"] is not None, "Task 1 should have a client."
-    assert task1["department"] is not None, "Task 1 should have a department."
-    assert task1["assignee"] is not None, "Task 1 should have an assignee."
-
-    task3 = data[2]
-    assert task3["client"] is None, "Task 3 should have no client."
-    assert task3["department"] is None, "Task 3 should have no department."
-    assert task3["assignee"] is None, "Task 3 should have no assignee."