Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to secondary tables relationships #218

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

Ckk3
Copy link
Contributor

@Ckk3 Ckk3 commented Nov 23, 2024

Fixes #19

Description

This PR fixes the relationships involving secondary tables by primarily updating the logic in loader.py.

Notably, a new flag, disabled_optimization_to_secondary_tables, has been introduced in the self._scalars_all function. This flag is required to disable SQLAlchemy's default optimization, which retrieves only the related_model values. In our case, this optimization must be disabled to retrieve values for both the self_model and the related_model.

This adjustment is necessary because the keys variable is used to match data from both the self_model and the related_model. Please review the implementation of this flag and its impact on the overall query behavior.

IMPORTANT
During development, I discovered an issue that causes the ModelConnection type to be duplicated when we have relationship. After investigation, it seems this problem is related to the Relay implementation rather than the secondary table logic.

To address this, I will create a separate issue. I’ve included a failing test in this PR, test_query_with_secondary_table_with_values_list, which demonstrates the issue.

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

Summary by Sourcery

Add support for secondary table relationships in the SQLAlchemy mapper, addressing a bug and enhancing the loader to handle these relationships efficiently. Include comprehensive tests to ensure the correct implementation of these features.

New Features:

  • Introduce support for querying secondary table relationships in the database schema.

Bug Fixes:

  • Fix an issue related to the handling of secondary table relationships in the SQLAlchemy mapper.

Enhancements:

  • Refactor the SQLAlchemy loader to handle secondary table relationships more efficiently by disabling certain optimizations when necessary.

Tests:

  • Add extensive tests to verify the correct functionality of secondary table relationships, including scenarios with different foreign keys and multiple secondary tables.

Summary by Sourcery

Add support for secondary table relationships in the SQLAlchemy mapper, addressing a bug and enhancing the loader to handle these relationships efficiently. Include comprehensive tests to ensure the correct implementation of these features.

New Features:

  • Introduce support for querying secondary table relationships in the database schema.

Bug Fixes:

  • Fix an issue related to the handling of secondary table relationships in the SQLAlchemy mapper.

Enhancements:

  • Refactor the SQLAlchemy loader to handle secondary table relationships more efficiently by disabling certain optimizations when necessary.

Tests:

  • Add extensive tests to verify the correct functionality of secondary table relationships, including scenarios with different foreign keys and multiple secondary tables.

@Ckk3 Ckk3 self-assigned this Nov 23, 2024
Copy link
Contributor

sourcery-ai bot commented Nov 23, 2024

Reviewer's Guide by Sourcery

This PR implements support for secondary table relationships in SQLAlchemy by modifying the loader logic to handle both standard and secondary table relationships correctly. The implementation includes disabling SQLAlchemy's default optimization when dealing with secondary tables to ensure proper data retrieval from both self_model and related_model.

ER Diagram for Secondary Table Relationships

erDiagram
    EMPLOYEE {
        INTEGER id PK
        STRING name
        STRING role
    }
    DEPARTMENT {
        INTEGER id PK
        STRING name
    }
    BUILDING {
        INTEGER id PK
        STRING name
    }
    EMPLOYEE ||--o{ DEPARTMENT : "belongs to"
    EMPLOYEE ||--o{ BUILDING : "works in"
    EMPLOYEE_DEPARTMENT_JOIN_TABLE {
        INTEGER employee_id PK
        INTEGER department_id PK
    }
    EMPLOYEE_BUILDING_JOIN_TABLE {
        INTEGER employee_id PK
        INTEGER building_id PK
    }
    EMPLOYEE ||--o{ EMPLOYEE_DEPARTMENT_JOIN_TABLE : ""
    DEPARTMENT ||--o{ EMPLOYEE_DEPARTMENT_JOIN_TABLE : ""
    EMPLOYEE ||--o{ EMPLOYEE_BUILDING_JOIN_TABLE : ""
    BUILDING ||--o{ EMPLOYEE_BUILDING_JOIN_TABLE : ""
Loading

Class Diagram for SQLAlchemy Models

classDiagram
    class Employee {
        +Integer id
        +String name
        +String role
        +Department department
        +Building building
    }
    class Department {
        +Integer id
        +String name
        +List~Employee~ employees
    }
    class Building {
        +Integer id
        +String name
        +List~Employee~ employees
    }
    Employee --> Department : "secondary"
    Employee --> Building : "secondary"
Loading

File-Level Changes

Change Details Files
Modified loader logic to support secondary table relationships
  • Added disabled_optimization_to_secondary_tables flag to _scalars_all function
  • Updated load_fn to handle secondary table relationships differently
  • Modified query construction for secondary tables to join self_model and related_model
  • Added special handling for grouping results from secondary table queries
src/strawberry_sqlalchemy_mapper/loader.py
Added new test fixtures and test cases for secondary table relationships
  • Created fixtures for different secondary table scenarios
  • Added tests for relationships with different foreign keys
  • Added tests for multiple secondary tables
  • Added tests for use_list=False scenarios
  • Added tests combining secondary and normal relationships
tests/conftest.py
tests/test_secondary_tables_query.py
Added error handling for invalid relationship configurations
  • Created InvalidLocalRemotePairs exception class
  • Added validation for local_remote_pairs in relationships
src/strawberry_sqlalchemy_mapper/exc.py
Updated mapper to handle secondary table schema generation
  • Added support for generating correct GraphQL types for secondary table relationships
  • Added tests for schema generation with different secondary table configurations
src/strawberry_sqlalchemy_mapper/mapper.py
tests/test_mapper.py

Assessment against linked issues

Issue Objective Addressed Explanation
#19 Fix SQLAlchemy relationships that fail when using secondary tables

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time. You can also use
    this command to specify where the summary should be inserted.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov-commenter
Copy link

codecov-commenter commented Nov 23, 2024

Codecov Report

Attention: Patch coverage is 92.36234% with 43 lines in your changes missing coverage. Please review.

Project coverage is 87.27%. Comparing base (2133fd7) to head (2a53474).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #218      +/-   ##
==========================================
+ Coverage   85.51%   87.27%   +1.75%     
==========================================
  Files          16       17       +1     
  Lines        1629     2137     +508     
  Branches      139      152      +13     
==========================================
+ Hits         1393     1865     +472     
- Misses        173      203      +30     
- Partials       63       69       +6     

Copy link

codspeed-hq bot commented Nov 23, 2024

CodSpeed Performance Report

Merging #218 will not alter performance

Comparing Ckk3:issue-19 (2a53474) with main (2133fd7)

Summary

✅ 1 untouched benchmarks

@Ckk3 Ckk3 marked this pull request as ready for review November 30, 2024 02:32
@botberry
Copy link
Member

botberry commented Nov 30, 2024

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Add support for secondary table relationships in SQLAlchemy mapper, addressing a bug and enhancing the loader to handle these relationships efficiently.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Ckk3 - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟡 Testing: 1 issue found
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

src/strawberry_sqlalchemy_mapper/loader.py Show resolved Hide resolved
tests/test_secondary_tables_query.py Outdated Show resolved Hide resolved
src/strawberry_sqlalchemy_mapper/loader.py Outdated Show resolved Hide resolved
Copy link
Member

@bellini666 bellini666 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial thoughts :)

One thing missing here is conforming to ruff/black style

@@ -45,12 +46,16 @@ def __init__(
"One of bind or async_bind_factory must be set for loader to function properly."
)

async def _scalars_all(self, *args, **kwargs):
async def _scalars_all(self, *args, disabled_optimization_to_secondary_tables=False, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: maybe call this enable_ and have True as the default?

if self._async_bind_factory:
async with self._async_bind_factory() as bind:
if disabled_optimization_to_secondary_tables is True:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick:

Suggested change
if disabled_optimization_to_secondary_tables is True:
if disabled_optimization_to_secondary_tables:

return (await bind.scalars(*args, **kwargs)).all()
else:
assert self._bind is not None
if disabled_optimization_to_secondary_tables is True:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick:

Suggested change
if disabled_optimization_to_secondary_tables is True:
if disabled_optimization_to_secondary_tables:


if not relationship.local_remote_pairs:
raise InvalidLocalRemotePairs(
f"{related_model.__name__} -- {self_model.__name__}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

polish: for ruff/black this parenthesis should be closed in the next line. I think you forgot to pre-commit install =P (ditto for the lines below)

Also, we are probably missing a lint check in here which runs ruff/black/etc (and maybe migrate to ruff formatter instead of black soon)

Comment on lines +121 to +124
def _build_query(*args):
return _build_normal_relationship_query(*args) if relationship.secondary is None else _build_relationship_with_secondary_table_query(*args)

query = _build_query(related_model, relationship, keys)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: total personal preference, so feel free to ignore. I feel like those 2 _buil functions could be defined in the module and query could be defined with the if directly, like query = _build_normal_relationship_query(related_model, relationship, keys) if relationship.secondary is None else _build_relationship_with_secondary_table_query(related_model, relationship, keys)

Even better to avoid *args in there as mypy/pyright can validate them

Comment on lines +129 to +133
if relationship.secondary is not None:
# We need to retrieve values from both the self_model and related_model. To achieve this, we must disable the default SQLAlchemy optimization that returns only related_model values. This is necessary because we use the keys variable to match both related_model and self_model.
rows = await self._scalars_all(query, disabled_optimization_to_secondary_tables=True)
else:
rows = await self._scalars_all(query)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion:

Suggested change
if relationship.secondary is not None:
# We need to retrieve values from both the self_model and related_model. To achieve this, we must disable the default SQLAlchemy optimization that returns only related_model values. This is necessary because we use the keys variable to match both related_model and self_model.
rows = await self._scalars_all(query, disabled_optimization_to_secondary_tables=True)
else:
rows = await self._scalars_all(query)
# We need to retrieve values from both the self_model and related_model.
# To achieve this, we must disable the default SQLAlchemy optimization
# that returns only related_model values. This is necessary because we
# use the keys variable to match both related_model and self_model.
rows = await self._scalars_all(query, disabled_optimization_to_secondary_tables=relationship.secondary is not None)

Comment on lines +512 to +516
[
getattr(self, local.key)
for local, _ in relationship.local_remote_pairs or []
if local.key
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: you can pass the iterator to the tuple directly, no need to create a list for that

Suggested change
[
getattr(self, local.key)
for local, _ in relationship.local_remote_pairs or []
if local.key
]
getattr(self, local.key)
for local, _ in relationship.local_remote_pairs or []
if local.key

Comment on lines +527 to +530
[
getattr(
self, str(local_remote_pairs_secondary_table_local.key)),
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: ditto



# TODO Investigate this test
@pytest.mark.skip("This test is currently failing because the Query with relay.ListConnection generates two DepartmentConnection, which violates the schema's expectations. After investigation, it appears this issue is related to the Relay implementation rather than the secondary table issue. We'll address this later. Additionally, note that the `result.data` may be incorrect in this test.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: strange...

I know that this lib will generate automatic connections for relations, and that indeed could conflict with the departments you are defining. But you don't have a departments relation inside Employee

Maybe the secondary table is generating such connection with that name? If so, is that correct/expected?

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

Successfully merging this pull request may close these issues.

SQLAlchemy Relationships fail when using secondary Table
4 participants