Skip to content

Conversation

@emanuelen5
Copy link
Member

@emanuelen5 emanuelen5 commented Oct 26, 2024

Closes #489

Summary by CodeRabbit

  • New Features

    • Enhanced SQL query handling by utilizing the text() construct for improved clarity and maintainability.
  • Bug Fixes

    • Streamlined member retrieval across various functions, improving efficiency and readability.
  • Chores

    • Updated dependency version for sqlalchemy to ensure compatibility with version 2.0.
    • Removed deprecated warning filters from configuration files.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 26, 2024

Warning

Rate limit exceeded

@emanuelen5 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 17 minutes and 7 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 0e9d5e6 and ed6e537.

📒 Files selected for processing (1)
  • api/src/migrate.py (4 hunks)

Walkthrough

The pull request introduces several modifications across multiple files, primarily focusing on updating the SQLAlchemy version constraint in requirements.txt to ~=2.0. Additionally, it simplifies member retrieval methods by replacing db_session.query(Model).get(id) with db_session.get(Model, id) in various functions, enhancing code clarity and efficiency. Other changes include improved SQL execution handling using the text() construct from SQLAlchemy, adjustments to relationship configurations in ORM models, and minor import statement reorganizations.

Changes

File Path Change Summary
api/requirements.txt Updated sqlalchemy version constraint from >=1.4.9,<2.0 to ~=2.0.
api/src/core/auth.py Simplified member retrieval in password_reset function.
api/src/core/models.py Consolidated imports and updated SQL execution methods to use text().
api/src/firstrun.py Simplified member retrieval in create_admin function.
api/src/init_db.py Updated SQL execution to use text() in clear_permission_cache.
api/src/member/member.py Simplified member retrieval in send_updated_member_info_email and set_pin_code functions; improved error handling in set_pin_code.
api/src/member/views.py Simplified member retrieval in current_member function.
api/src/membership/member_entity.py Updated SQL execution to use text() and improved error handling in create method.
api/src/membership/models.py Updated relationship definitions with cascade_backrefs=False and adjusted imports.
api/src/messages/models.py Consolidated import statements.
api/src/migrate.py Updated SQL execution to use text() for various SQL statements.
api/src/multiaccessy/invite.py Simplified member retrieval in check_labaccess_requirements and ensure_accessy_labaccess.
api/src/multiaccessy/models.py Consolidated import statements.
api/src/pytest.ini Removed SQLAlchemy 2.0 compatibility warning filter.
api/src/quiz/models.py Updated relationship definitions with cascade_backrefs=False.
api/src/quiz/views.py Enhanced SQL handling in quiz_statistics and simplified member retrieval in answer_question.
api/src/service/auth.py Simplified token retrieval in authenticate_request function.
api/src/service/entity.py Simplified entity retrieval in read, _update_internal, and delete methods.
api/src/shell_with_db.py Removed unnecessary import statements.
api/src/shop/email.py Simplified member retrieval in email sending functions.
api/src/shop/filters.py Simplified member retrieval in filter_no_subscription_active.
api/src/shop/models.py Updated relationship definitions with cascade_backrefs=False.
api/src/shop/ordered_entity.py Updated SQL execution to use text() in create method.
api/src/shop/pay.py Simplified member retrieval in payment-related functions.
api/src/shop/shop_data.py Integrated TransactionContent model into transaction handling logic.
api/src/shop/stripe_customer.py Simplified member retrieval in delete_stripe_customer function.
api/src/shop/stripe_event.py Simplified member retrieval in multiple functions related to Stripe events.
api/src/shop/stripe_payment_intent.py Simplified member and transaction retrieval in payment functions.
api/src/shop/stripe_subscriptions.py Simplified member retrieval in subscription-related functions.
api/src/shop/transactions.py Updated SQL execution to use text() and simplified member retrieval in transaction functions.
api/src/statistics/maker_statistics.py Added text import and updated SQL execution in lasertime function.
api/src/systest/api/password_reset_test.py Simplified member retrieval in test function.
api/src/test_aid/db.py Added text import and updated SQL execution in get_member_number method.
api/src/multiaccessy/sync.py Simplified member retrieval in get_wanted_access and sync functions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant DB

    Client->>API: Request password reset
    API->>DB: Get member by token
    DB-->>API: Return member
    API-->>Client: Send password reset email
Loading

Assessment against linked issues

Objective Addressed Explanation
Upgrade to SQLAlchemy 2 (Issue #489)
Use replacement style in the form of select No changes were made to implement the select style.

Possibly related PRs

Poem

🐇 In the code where rabbits play,
SQLAlchemy leads the way.
With get we hop, no need to query,
Simplified paths, oh so cheery!
Dependencies updated, all in line,
Hopping forward, our code will shine! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@emanuelen5

This comment was marked as resolved.

@emanuelen5 emanuelen5 force-pushed the sqlalchemy-2-upgrade branch from 2df5bab to 4b72c44 Compare October 27, 2024 13:51
@emanuelen5 emanuelen5 changed the title Sqlalchemy 2 upgrade Upgrade sqlalchemy to version 2 Oct 27, 2024
@emanuelen5 emanuelen5 marked this pull request as ready for review October 27, 2024 19:10
@emanuelen5
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2024

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Outside diff range and nitpick comments (37)
api/requirements.txt (1)

4-4: Consider pinning to a specific minor version for better stability.

While ~=2.0 allows for compatible updates, it's recommended to pin to a specific minor version (e.g., sqlalchemy~=2.0.23) to ensure consistent behavior across environments and prevent unexpected issues from patch updates.

api/src/shop/email.py (1)

13-13: Consider adding error handling for non-existent members.

The SQLAlchemy 2 syntax update looks good. However, db_session.get() will return None for non-existent members, which could lead to a NoneType error in the subsequent send_message call.

Consider adding error handling:

-    member = db_session.get(Member, member_id)
+    member = db_session.get(Member, member_id)
+    if member is None:
+        raise ValueError(f"Member with id {member_id} not found")
api/src/shop/ordered_entity.py (1)

Line range hint 25-27: Consider modernizing the display_order query.

The display_order query still uses the legacy query() style. Consider updating to SQLAlchemy 2's new style:

-                data["display_order"] = (db_session.query(func.max(self.model.display_order)).scalar() or 0) + 1
+                result = db_session.execute(
+                    select(func.max(self.model.display_order))
+                ).scalar()
+                data["display_order"] = (result or 0) + 1

Also, consider documenting that this implementation is MySQL-specific due to the locking mechanism used.

api/src/shop/filters.py (2)

Line range hint 34-45: Consider improving error messages for better user experience.

The error messages could be more user-friendly and provide clearer guidance:

-                    "You already have a makerspace access subscription. You must cancel your subscription if you want to buy invidual makerspace access months."
+                    "You already have an active makerspace access subscription. Please cancel your current subscription before purchasing individual makerspace access months."
-                    "You already have a base membership subscription. You must cancel your subscription if you want to buy invidual base membership years."
+                    "You already have an active base membership subscription. Please cancel your current subscription before purchasing individual base membership years."

Also, there are typos in both messages: "invidual" should be "individual".


Based on the shell script results, I can generate the final response as we have sufficient evidence showing the consistent pattern of null checks across the codebase.

Add null check for non-existent member

The codebase consistently checks for non-existent members when using db_session.get(Member, member_id). For example:

  • shop/transactions.py: if member is None: raise BadRequest(f"Unable to find member with id {member_id}")
  • shop/stripe_subscriptions.py: if member is None: raise BadRequest(f"Unable to find member with id {member_id}")
  • shop/pay.py: if member is None: raise BadRequest(f"Unable to find member with id {member_id}")
 def filter(cart_item: "CartItem", member_id: int) -> None:
     member: Member = db_session.get(Member, member_id)
+    if member is None:
+        raise BadRequest(f"Unable to find member with id {member_id}")
     if sub == SubscriptionType.LAB:
🔗 Analysis chain

Line range hint 34-47: Verify error handling for non-existent members.

The db_session.get() method returns None for non-existent records. The current implementation might raise an AttributeError when accessing stripe_labaccess_subscription_id or stripe_membership_subscription_id on a None value.

Consider adding a null check:

 def filter(cart_item: "CartItem", member_id: int) -> None:
     member: Member = db_session.get(Member, member_id)
+    if member is None:
+        raise BadRequest(f"Member with ID {member_id} not found")
     if sub == SubscriptionType.LAB:
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if there's any null check before accessing member attributes
# across the codebase to ensure consistent error handling.

# Test 1: Look for similar member retrieval patterns
rg -A 5 "db_session\.get\(Member," 

# Test 2: Look for null checks after member retrieval
ast-grep --pattern 'if $member is None: $$$'

Length of output: 9037

api/src/service/auth.py (1)

Line range hint 42-43: Consider wrapping expired token cleanup in a transaction.

The deletion of expired tokens could potentially affect concurrent requests. Consider wrapping this operation in a transaction with appropriate isolation level.

    if access_token.expires < now:
+       with db_session.begin():
            db_session.query(AccessToken).filter(AccessToken.expires < now).delete()
        raise Unauthorized("Unauthorized, expired access token.", fields="bearer", what=EXPIRED)
api/src/core/models.py (2)

Line range hint 41-64: Consider using SQLAlchemy ORM features for the Login table.

While the current implementation works, consider creating a proper SQLAlchemy model for the Login table in the future. Even without a primary key, you can use:

  • __table_args__ = {'primary_key': []} to define a table without PK
  • Column definitions with proper types
  • Query methods using SQLAlchemy's expression language

This would provide:

  1. Better type safety
  2. Query composition capabilities
  3. Consistency with other models

Based on the comprehensive analysis of the codebase, I can now provide a final response about the SQLAlchemy upgrade impact:

Several SQLAlchemy 1.x patterns need updates for 2.x compatibility

The codebase contains multiple patterns that require updates for SQLAlchemy 2.x:

  • Query-style ORM operations need to be replaced with select()-based queries:

    • Numerous instances of db_session.query() should be updated to db_session.execute(select())
    • Direct filter() and filter_by() calls need to be wrapped in select()
  • Session operations need review:

    • Multiple direct db_session.add() calls should be evaluated for bulk operations
    • commit() and flush() usage patterns should be reviewed for transaction management
    • Raw execute() calls should use text() consistently
  • Relationship loading patterns:

    • Several relationship() definitions with lazy="dynamic" need updates
    • joinedload() usage should be evaluated for potential selectinload() replacement
    • Relationship cascade settings should be reviewed for SQLAlchemy 2.x behavior changes
🔗 Analysis chain

Line range hint 1-64: Verify SQLAlchemy 2.x compatibility across the codebase.

While the changes in this file look correct, let's verify other potential SQLAlchemy 2 incompatibilities:


Based on the search results, I can see several patterns that need to be updated for SQLAlchemy 2.x compatibility:

  1. Many raw query() calls that need to be replaced with select()
  2. filter() and filter_by() usage without select()
  3. joinedload() usage that should be reviewed for potential selectinload() replacement

Let me verify one more aspect to ensure we have a complete picture:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for common SQLAlchemy 1.x patterns that need updates in 2.x

echo "Checking for deprecated Query usage..."
rg "db_session\.query\(" -A 2

echo "Checking for deprecated filter/filter_by usage without select..."
rg "\.filter\(|\.filter_by\(" -A 2

echo "Checking for raw execute calls without text()..."
rg "execute\(['\"]" -A 2

echo "Checking for joinedload without selectinload..."
rg "joinedload\(" -A 2

Length of output: 242588


Script:

#!/bin/bash
# Check for other SQLAlchemy 1.x patterns
echo "Checking for session.add/delete usage..."
rg "db_session\.(add|delete)\(" -A 2

echo "Checking for relationship loading patterns..."
rg "relationship\(" -A 2

echo "Checking for session.commit usage..."
rg "db_session\.commit" -A 2

Length of output: 21398

api/src/quiz/models.py (2)

50-50: Consider documenting the cascade behavior.

Since the cascade behavior is now explicitly configured, it would be helpful to add a comment explaining the intended behavior for future maintainers.

Example documentation:

-    question = relationship(QuizQuestion, backref="options", cascade_backrefs=False)
+    # Disable automatic cascading through backrefs to prevent unexpected deletions
+    # Child records (options) won't be automatically deleted when parent (question) is deleted
+    question = relationship(QuizQuestion, backref="options", cascade_backrefs=False)

Also applies to: 69-69


Line range hint 1-77: Consider adding type hints for better IDE support.

While reviewing the SQLAlchemy 2 changes, I noticed that the models could benefit from type hints, which are better supported in SQLAlchemy 2.x. This would improve IDE support and make the code more maintainable.

Example for the Question relationship:

from typing import List

class QuizQuestion(Base):
    options: List["QuizQuestionOption"] = relationship("QuizQuestionOption", backref="question", cascade_backrefs=False)
api/src/multiaccessy/invite.py (1)

70-71: Consider removing redundant assertion

The assert member is not None check is redundant because this code is only reached after check_labaccess_requirements verifies the member exists. If the member doesn't exist, an AccessyInvitePreconditionFailed exception would be raised earlier in the function.

 member = db_session.get(Member, member_id)
-assert member is not None
api/src/member/member.py (2)

40-40: LGTM! Consider adding error handling for non-existent members.

The update to use db_session.get() aligns well with SQLAlchemy 2's simplified API. However, consider adding error handling for cases where the member doesn't exist.

-    member = db_session.get(Member, member_id)
+    member = db_session.get(Member, member_id)
+    if member is None:
+        raise BadRequest(f"Member with id {member_id} not found")

Line range hint 58-58: Update member retrieval to use SQLAlchemy 2 pattern.

For consistency with other changes and SQLAlchemy 2 patterns, update the member retrieval.

-    member: Member = db_session.query(Member).get(member_id)
+    member: Member = db_session.get(Member, member_id)
api/src/member/views.py (1)

43-45: LGTM! Consider enhancing error handling.

The update to SQLAlchemy 2.0's db_session.get() syntax is correct. However, the double assertion pattern could be simplified with more descriptive error handling.

Consider this more descriptive error handling approach:

-    m2 = db_session.get(Member, g.user_id)
-    assert m2 is not None
-    m["has_password"] = m2.password is not None
+    m2 = db_session.get(Member, g.user_id)
+    if m2 is None:
+        raise ValueError(f"Member {g.user_id} not found in database")
+    m["has_password"] = m2.password is not None
api/src/shop/shop_data.py (1)

Line range hint 1-166: Consider further SQLAlchemy 2 modernization opportunities.

While the current changes improve the eager loading patterns, there are opportunities to further modernize the code using SQLAlchemy 2's preferred patterns:

  1. Replace db_session.query() with select() statements
  2. Use scalars() for single-model queries
  3. Leverage SQL expression language improvements

Example transformation:

# Before
query = (
    db_session.query(Transaction)
    .filter(Transaction.member_id == member_id)
    .order_by(desc(Transaction.id))
)

# After
stmt = (
    select(Transaction)
    .where(Transaction.member_id == member_id)
    .order_by(desc(Transaction.id))
)
result = db_session.execute(stmt).scalars()

Would you like me to provide more specific examples for each query in this file?

api/src/core/auth.py (6)

Line range hint 27-37: Update member retrieval in get_member_by_user_identification.

For consistency with SQLAlchemy 2 style, consider updating the query patterns in this function:

     try:
         if user_identification.isdigit():
-            return db_session.query(Member).filter_by(member_number=int(user_identification), deleted_at=None).one()
+            return db_session.execute(
+                db_session.select(Member)
+                .filter_by(member_number=int(user_identification), deleted_at=None)
+            ).scalar_one()
 
-        return db_session.query(Member).filter_by(email=user_identification, deleted_at=None).one()
+        return db_session.execute(
+            db_session.select(Member)
+            .filter_by(email=user_identification, deleted_at=None)
+        ).scalar_one()

Line range hint 146-147: Update delete operation to use SQLAlchemy 2 style.

Consider updating the delete operation to use the new SQLAlchemy 2 style:

-    db_session.query(PasswordResetToken).filter(PasswordResetToken.id == password_reset_token.id).delete()
+    db_session.execute(
+        db_session.delete(PasswordResetToken)
+        .filter_by(id=password_reset_token.id)
+    )

Line range hint 171-174: Update count query to use SQLAlchemy 2 style.

Consider updating the count query to use the new SQLAlchemy 2 style:

-    count = (
-        db_session.query(AccessToken).filter(AccessToken.user_id == user_id, AccessToken.access_token == token).delete()
-    )
+    result = db_session.execute(
+        db_session.delete(AccessToken)
+        .filter_by(user_id=user_id, access_token=token)
+    )
+    count = result.rowcount

Line range hint 180-189: Update list query to use SQLAlchemy 2 style.

Consider updating the list query to use the new SQLAlchemy 2 style:

     return [
         dict(
             access_token=access_token.access_token,
             browser=access_token.browser,
             ip=access_token.ip,
             expires=access_token.expires.isoformat(),
         )
-        for access_token in db_session.query(AccessToken).filter(AccessToken.user_id == user_id)
+        for access_token in db_session.execute(
+            db_session.select(AccessToken)
+            .filter_by(user_id=user_id)
+        ).scalars()
     ]

Line range hint 193-203: Update service token query to use SQLAlchemy 2 style.

Consider updating the service token query to use the new SQLAlchemy 2 style:

     try:
-        access_token = db_session.query(AccessToken).filter_by(user_id=user_id).one()
+        access_token = db_session.execute(
+            db_session.select(AccessToken)
+            .filter_by(user_id=user_id)
+        ).scalar_one()
         access_token.access_token = generate_token()
         db_session.add(access_token)
     except NoResultFound:
         raise NotFound()

Line range hint 207-217: Update service tokens list query to use SQLAlchemy 2 style.

Consider updating the service tokens list query to use the new SQLAlchemy 2 style:

     return [
         dict(
             user_id=access_token.user_id,
             service_name=SERVICE_NAMES.get(access_token.user_id, "unknown service"),
             access_token=access_token.access_token,
             permissions=",".join(SERVICE_PERMISSIONS.get(access_token.user_id, [])),
         )
-        for access_token in db_session.query(AccessToken).filter(AccessToken.user_id < 0)
+        for access_token in db_session.execute(
+            db_session.select(AccessToken)
+            .filter(AccessToken.user_id < 0)
+        ).scalars()
     ]
api/src/shop/stripe_payment_intent.py (1)

Line range hint 116-196: Consider configuring transaction isolation level for payment processing.

Payment processing typically requires serializable isolation to prevent concurrent modifications. Consider configuring the transaction isolation level explicitly:

from sqlalchemy import event

@event.listens_for(db_session, 'after_begin')
def receive_after_begin(session, transaction, connection):
    if 'stripe_payment_intent' in connection.info:
        connection.execute('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE')

This ensures that payment-related database operations are properly isolated, preventing race conditions during concurrent payment processing.

api/src/test_aid/db.py (1)

Line range hint 28-266: Consider reviewing other SQLAlchemy operations.

While the raw SQL query has been updated, there are several other SQLAlchemy operations in this file that might benefit from updating to SQLAlchemy 2.x style:

  1. The delete_category method uses the Query API:
db_session.query(ProductCategory).filter(ProductCategory.id == category_id).delete()
  1. The delete_product method uses similar Query API patterns.

Consider updating these to use the new SQLAlchemy 2.x style with select() and delete() constructs.

Example refactor for delete_category:

from sqlalchemy import delete

# ...
    def delete_category(self, id: Optional[int] = None) -> None:
        assert self.category is not None
        category_id = id or self.category.id
        assert category_id is not None
        stmt = delete(ProductCategory).where(ProductCategory.id == category_id)
        db_session.execute(stmt)
        db_session.flush()
api/src/quiz/views.py (2)

287-289: LGTM with suggestions: Raw SQL query updated correctly for SQLAlchemy 2.

The change to use text() wrapper with parameterized queries is correct for SQLAlchemy 2. However, consider the following improvement:

The current query uses MySQL-specific time functions (TIME_TO_SEC, TIMEDIFF). For better database portability, consider using SQLAlchemy's built-in functions. Here's an example:

from sqlalchemy import func
# Replace the raw SQL with SQLAlchemy expression language
query = (
    db_session.query(
        func.extract('epoch', 
            func.max(QuizAnswer.created_at) - func.min(QuizAnswer.created_at)
        ).label('t')
    )
    .join(QuizQuestion)
    .filter(QuizQuestion.quiz_id == quiz_id)
    .group_by(QuizAnswer.member_id)
    .order_by('t')
)
seconds_to_answer_quiz = query.all()

This approach would:

  1. Work across different database backends
  2. Leverage SQLAlchemy's query builder
  3. Maintain type safety

Line range hint 1-324: Consider gradually migrating to SQLAlchemy 2's new query style.

While the current changes correctly address the immediate needs for SQLAlchemy 2 compatibility, there are several query patterns throughout the file that could benefit from migrating to SQLAlchemy 2's new style using select(). This isn't blocking but could be done in a future iteration.

Example pattern to consider updating:

# Current style
db_session.query(QuizQuestion).filter(...)

# SQLAlchemy 2 style
from sqlalchemy import select
db_session.execute(
    select(QuizQuestion).filter(...)
).scalars()

Would you like me to create a GitHub issue to track this technical debt?

api/src/shop/models.py (1)

Line range hint 21-278: Consider implementing a comprehensive testing strategy for the SQLAlchemy 2 upgrade.

The consistent addition of cascade_backrefs=False across all relationships represents a significant change in how SQLAlchemy handles relationship cascades. To ensure a smooth upgrade:

  1. Consider adding integration tests that specifically verify cascade behaviors
  2. Document any changes in cascade behavior for future reference
  3. Plan for a careful deployment strategy, possibly with a feature flag
  4. Monitor the application logs after deployment for any cascade-related issues

This will help maintain data integrity during and after the upgrade.

api/src/shop/pay.py (3)

164-167: Improve error message for member lookup.

The error message should be more user-friendly and specific about the issue.

Consider this improvement:

-    if member is None:
-        raise BadRequest(f"Unable to find member with id {member_id}")
+    if member is None:
+        raise BadRequest("Your member account could not be found. Please ensure you are logged in correctly.")

209-212: Document the subscription dependency rule.

While the member retrieval change is correct, this function contains important business logic about lab subscription dependencies. Consider adding a docstring to document this rule.

Add documentation:

 def cancel_subscriptions(data: CancelSubscriptionsRequest, user_id: int) -> None:
+    """Cancel the specified subscriptions for a member.
+    
+    Note: Due to business rules, canceling a membership subscription will automatically
+    cancel any lab subscription, as lab access requires an active membership.
+    
+    Args:
+        data: The cancellation request containing subscription types to cancel
+        user_id: The ID of the member
+    Raises:
+        BadRequest: If the member is not found
+    """

Line range hint 1-338: Consider adding integration tests for payment flows.

Given the critical nature of payment processing and subscription management, and considering the current test failures, it would be beneficial to add comprehensive integration tests that cover:

  1. Payment flows with insufficient funds
  2. Subscription cancellation with dependencies
  3. Member cleanup scenarios

Consider implementing these tests using the SQLAlchemy 2.0 patterns and mocked Stripe responses. This will help ensure the upgrade doesn't introduce regressions in these critical paths.

api/src/statistics/maker_statistics.py (1)

Line range hint 1-400: Consider architectural improvements for database access.

While the current changes address the immediate SQLAlchemy 2 upgrade needs, consider these future improvements:

  1. Implement repository pattern to encapsulate database access
  2. Create database-agnostic query builders for complex queries
  3. Move common database operations into reusable utilities
api/src/service/entity.py (2)

Line range hint 338-346: LGTM! Session.get() correctly implemented in delete operation.

The migration to session.get() is properly done while preserving the soft delete functionality.

Consider adding type hints to improve code clarity:

-    def delete(self, entity_id: int, commit: bool = True) -> None:
+    def delete(self, entity_id: int, commit: bool = True) -> None:
+        """Soft delete an entity by setting its deleted_at timestamp.
+        
+        Args:
+            entity_id: The primary key of the entity to delete
+            commit: Whether to commit the transaction immediately
+        
+        Raises:
+            NotFound: If the entity doesn't exist
+        """
         entity = db_session.get(self.model, entity_id)

Migration to SQLAlchemy 2.0's Session.get() needs attention in member.py

Found one remaining instance of the deprecated query().get() pattern that needs to be updated:

  • api/src/member/member.py: member: Member = db_session.query(Member).get(member_id)

The rest of the codebase (27 occurrences) has been properly migrated to use db_session.get(), including the changes in service/entity.py.

🔗 Analysis chain

Line range hint 307-311: LGTM! Correctly migrated to SQLAlchemy 2.0's Session.get() method.

The change from query().get() to session.get() follows SQLAlchemy 2.0's best practices for retrieving single entities by primary key.

Let's verify consistent usage of Session.get() across the codebase:


Let me gather more targeted information to verify the SQLAlchemy migration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining instances of the deprecated query().get() pattern
rg "query\([^)]+\)\.get\("

# Search for the new Session.get() pattern to confirm migration
rg "db_session\.get\("

Length of output: 2549


Script:

#!/bin/bash
# Search for any remaining instances of query().get() pattern specifically
rg "\.query\([^)]+\)\.get\(" --type py

# Search for the new session.get() pattern
rg "\.get\([^)]+\)" --type py -B 1 | grep -E "(session|db_session)\.get\("

Length of output: 2539

api/src/shop/transactions.py (2)

112-118: Consider adding type hints for SQL parameters.

The SQL query execution has been correctly updated to use text() for SQLAlchemy 2 compatibility. The parameterization is secure against SQL injection.

Consider adding type hints for the parameters to improve maintainability:

 db_session.execute(
     text(
         """
         INSERT INTO webshop_transaction_actions (content_id, action_type, value, status)
         SELECT :content_id AS content_id, action_type, SUM(:count * value) AS value, :pending AS status
         FROM webshop_product_actions WHERE product_id=:product_id AND deleted_at IS NULL GROUP BY action_type
         """
     ),
-    {
-        "content_id": content.id,
-        "count": content.count,
-        "pending": TransactionAction.PENDING,
-        "product_id": content.product_id,
-    },
+    {
+        "content_id": content.id,  # type: int
+        "count": content.count,    # type: int
+        "pending": TransactionAction.PENDING,  # type: str
+        "product_id": content.product_id,      # type: int
+    },
 )

SQLAlchemy 2.x migration is incomplete in this file

The code still uses some legacy SQLAlchemy 1.x patterns that should be updated to 2.x style:

  • db_session.query() should be replaced with db_session.execute(select())
  • Direct get() calls should use db_session.get() (this is already correctly updated)

Specific locations needing updates:

  • Line ~50: db_session.query(Transaction).filter(...)
  • Line ~150: db_session.query(TransactionAction, TransactionContent, Transaction).join(...)
  • Line ~400: db_session.query(Product).filter(...)
🔗 Analysis chain

Line range hint 1-424: Verify complete migration to SQLAlchemy 2.x style queries.

Let's verify that all SQLAlchemy queries in this file have been updated to the new style.


Let me check the specific filter usage to ensure it follows SQLAlchemy 2.x style.


Let me check for the recommended SQLAlchemy 2.x select() style usage pattern to compare against the current implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining old-style SQLAlchemy queries that might need updating

# Test 1: Check for old-style .get() usage
echo "Checking for old-style .get() usage..."
rg -l "query\([^)]+\)\.get\(" api/src/shop/transactions.py

# Test 2: Check for raw SQL queries not using text()
echo "Checking for raw SQL queries not using text()..."
rg -l "execute\(['\"]" api/src/shop/transactions.py

# Test 3: Check for old-style filter/filter_by usage
echo "Checking for potential old-style filter usage..."
rg -l "query\([^)]+\)\.filter" api/src/shop/transactions.py

Length of output: 508


Script:

#!/bin/bash
# Check the specific filter usage contexts
rg -A 3 "query\([^)]+\)\.filter" api/src/shop/transactions.py

Length of output: 307


Script:

#!/bin/bash
# Check for modern select() style usage
echo "Checking for modern select() usage..."
rg "select\(" api/src/shop/transactions.py

# Check for other query patterns
echo "Checking other query patterns..."
rg "db_session\." api/src/shop/transactions.py

Length of output: 939

api/src/firstrun.py (3)

Line range hint 42-42: Consider updating remaining SQLAlchemy query patterns.

For consistency with SQLAlchemy 2's new style, consider updating these query patterns:

- db_session.query(func.max(model.display_order)).scalar()
+ db_session.scalar(select(func.max(model.display_order)))

- db_session.query(model).filter_by(**kwargs).first()
+ db_session.scalars(select(model).filter_by(**kwargs)).first()

- db_session.query(Permission)
+ db_session.scalars(select(Permission))

- db_session.query(Product).filter_by(category_id=tools_category.id).all()
+ db_session.scalars(select(Product).filter_by(category_id=tools_category.id)).all()

- db_session.query(Product).all()
+ db_session.scalars(select(Product)).all()

Note: Don't forget to add the import:

from sqlalchemy import select

Also applies to: 53-53, 82-82, 386-386, 516-516


Line range hint 459-459: Address TODO comment about gift card product association.

The comment indicates a pending implementation for gift card product association. This should be tracked and addressed, especially since it's in a data initialization script.

Would you like me to help create a GitHub issue to track this TODO item?


Line range hint 1-577: Consider these improvements for better maintainability.

  1. Add type hints to function parameters and return values for better code maintainability.
  2. Consider moving hardcoded values (prices, member numbers, etc.) to configuration.
  3. Consider splitting test data creation into a separate module to keep the core initialization script focused.

Example of adding type hints:

- def next_display_order(model) -> int:
+ def next_display_order(model: Any) -> int:

- def get_or_create(model, defaults=None, **kwargs):
+ def get_or_create(model: Any, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any:
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 50ec053 and 0d651ac.

📒 Files selected for processing (34)
  • api/requirements.txt (1 hunks)
  • api/src/core/auth.py (1 hunks)
  • api/src/core/models.py (2 hunks)
  • api/src/firstrun.py (1 hunks)
  • api/src/init_db.py (1 hunks)
  • api/src/member/member.py (1 hunks)
  • api/src/member/views.py (1 hunks)
  • api/src/membership/member_entity.py (2 hunks)
  • api/src/membership/models.py (9 hunks)
  • api/src/messages/models.py (1 hunks)
  • api/src/migrate.py (4 hunks)
  • api/src/multiaccessy/invite.py (2 hunks)
  • api/src/multiaccessy/models.py (1 hunks)
  • api/src/pytest.ini (0 hunks)
  • api/src/quiz/models.py (3 hunks)
  • api/src/quiz/views.py (3 hunks)
  • api/src/service/auth.py (1 hunks)
  • api/src/service/entity.py (3 hunks)
  • api/src/shell_with_db.py (1 hunks)
  • api/src/shop/email.py (1 hunks)
  • api/src/shop/filters.py (1 hunks)
  • api/src/shop/models.py (6 hunks)
  • api/src/shop/ordered_entity.py (3 hunks)
  • api/src/shop/pay.py (4 hunks)
  • api/src/shop/shop_data.py (3 hunks)
  • api/src/shop/stripe_customer.py (1 hunks)
  • api/src/shop/stripe_event.py (3 hunks)
  • api/src/shop/stripe_payment_intent.py (2 hunks)
  • api/src/shop/stripe_subscriptions.py (2 hunks)
  • api/src/shop/transactions.py (4 hunks)
  • api/src/statistics/maker_statistics.py (2 hunks)
  • api/src/systest/api/password_reset_test.py (1 hunks)
  • api/src/test_aid/db.py (2 hunks)
  • public/src/public.py (1 hunks)
💤 Files with no reviewable changes (1)
  • api/src/pytest.ini
✅ Files skipped from review due to trivial changes (3)
  • api/src/messages/models.py
  • api/src/multiaccessy/models.py
  • api/src/shell_with_db.py
🔇 Additional comments (49)
api/requirements.txt (1)

4-4: Verify compatibility with type packages.

The codebase includes both types-sqlalchemy and sqlalchemy-stubs. Let's verify their compatibility with SQLAlchemy 2.0.

✅ Verification successful

Based on the search results, I can see that the codebase is using SQLAlchemy 2.0 compatible query patterns. The key findings are:

  1. The codebase uses SQLAlchemy 2.0 style queries with query() instead of legacy Model.query
  2. Session management is properly handled with scoped sessions
  3. Relationship loading is done using modern patterns like joinedload() and contains_eager()
  4. No deprecated SQLAlchemy 1.x patterns are found

Let me verify if there are any type stubs installed by searching for type packages in requirements files:


Both type packages are compatible with SQLAlchemy 2.0 - no action needed

The codebase already has both types-sqlalchemy and sqlalchemy-stubs installed, and both packages support SQLAlchemy 2.0. The codebase is using modern SQLAlchemy 2.0 query patterns and proper type hints. No compatibility issues are found.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if the installed type packages support SQLAlchemy 2.0

# Test: Search for version constraints in package metadata
rg -A 5 "SQLAlchemy" $(fd -t f "setup.py|pyproject.toml|setup.cfg" | grep -E "types-sqlalchemy|sqlalchemy-stubs")

Length of output: 227929


Script:

#!/bin/bash
# Search for SQLAlchemy type packages in requirements files
rg "types-sqlalchemy|sqlalchemy-stubs" -A 2 $(fd -t f "requirements.*\.txt")

Length of output: 356

api/src/shop/email.py (1)

21-21: Similar error handling needed here.

The same SQLAlchemy 2 syntax update and error handling concerns apply to this function.

api/src/shop/ordered_entity.py (1)

5-5: LGTM! Proper import for SQLAlchemy text construct.

The addition of text import aligns with SQLAlchemy 2's recommended approach for raw SQL execution.

api/src/shop/filters.py (1)

34-34: LGTM! Correct usage of SQLAlchemy 2's session.get() method.

The change from db_session.query(Member).get(member_id) to db_session.get(Member, member_id) aligns with SQLAlchemy 2's recommended patterns for retrieving single records by primary key.

api/src/init_db.py (2)

14-14: LGTM: SQLAlchemy text import added correctly.

The addition of the text import aligns with SQLAlchemy 2.x's recommended approach for executing raw SQL queries.


Line range hint 1-85: Verify other raw SQL queries in the codebase.

While this file has been updated to use SQLAlchemy 2.x's text() construct, let's verify that all other raw SQL queries in the codebase have been similarly updated.

✅ Verification successful

Let's gather more specific information about the raw SQL queries.


All raw SQL queries are properly using SQLAlchemy's text() construct

Based on the verification results:

  1. All direct SQL executions in the codebase are already using the text() construct from SQLAlchemy
  2. The text import is properly declared in all relevant files
  3. No instances of raw SQL strings being executed without text() were found

While there are many files using the .query() style queries, these are ORM queries rather than raw SQL, so they don't require the text() wrapper.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for potential raw SQL queries that might need the text() wrapper

# Test 1: Look for direct session.execute calls that might need text()
echo "Searching for potential raw SQL queries..."
rg -l "session\.execute\(['\"]" --type py

# Test 2: Look for query() usage that might need updating to select()
echo "Searching for old-style query() usage..."
rg -l "\.query\(" --type py

Length of output: 1807


Script:

#!/bin/bash
# Search for direct session.execute calls with SQL strings, showing context
echo "=== Direct SQL executions ==="
rg -A 2 "session\.execute\(['\"].*" --type py

# Search for text() import and usage to see if it's already properly used
echo -e "\n=== text() imports and usage ==="
rg "from sqlalchemy import.*text" --type py
rg "text\(['\"].*" --type py

Length of output: 2344

api/src/service/auth.py (1)

36-36: LGTM! Correctly migrated to SQLAlchemy 2's simplified get syntax.

The change from db_session.query(AccessToken).get(token) to db_session.get(AccessToken, token) aligns with SQLAlchemy 2's recommended approach for retrieving single objects by primary key.

api/src/systest/api/password_reset_test.py (1)

79-79: LGTM! Correct implementation of SQLAlchemy 2 session.get() pattern

The change from db_session.query(Member).get(member_id) to db_session.get(Member, member_id) aligns with SQLAlchemy 2's recommended approach for retrieving entities by primary key.

api/src/core/models.py (1)

3-3: LGTM: Import consolidation follows SQLAlchemy 2 patterns.

The consolidation of SQLAlchemy imports is consistent with the upgrade to version 2.

api/src/quiz/models.py (2)

3-3: LGTM! Import consolidation aligns with SQLAlchemy 2.x style.

The consolidated import statement is cleaner and follows modern SQLAlchemy conventions.


50-50: Verify cascade behavior for delete operations.

The addition of cascade_backrefs=False to relationships is a good practice for SQLAlchemy 2.x as it makes the cascade behavior explicit. This change prevents unexpected cascading deletions through backrefs, which was the default behavior in SQLAlchemy 1.x.

However, we should verify that this doesn't break existing delete operations in the application.

Let's check for any explicit delete operations that might be affected:

Also applies to: 69-69

✅ Verification successful

Let me check the specific delete operations in the views file to understand the impact better.


Setting cascade_backrefs=False is safe in this context

The codebase uses soft deletes (via deleted_at column) rather than actual record deletion for both QuizQuestion and QuizQuestionOption. The relationship modification with cascade_backrefs=False won't impact the existing functionality because:

  1. No hard delete operations are performed in the views
  2. All deletions are handled through deleted_at timestamp columns
  3. The queries properly filter out soft-deleted records using deleted_at == None
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for delete operations on QuizQuestion, QuizQuestionOption, or QuizAnswer
rg -l "delete.*(?:QuizQuestion|QuizQuestionOption|QuizAnswer)" --type py

# Search for cascade delete configurations in related models
ast-grep --pattern 'relationship($_, backref=$_, cascade=$_)'

Length of output: 160


Script:

#!/bin/bash
# Check the delete operations in views.py with context
rg "delete.*(?:QuizQuestion|QuizQuestionOption|QuizAnswer)" api/src/quiz/views.py -B 2 -A 2

# Let's also check all relationship definitions in models.py to understand the complete cascade setup
rg "relationship\(" api/src/quiz/models.py -B 1 -A 1

Length of output: 763

api/src/multiaccessy/invite.py (2)

31-31: LGTM! Correctly updated to SQLAlchemy 2 pattern

The change from query().get() to get() follows SQLAlchemy 2's recommended pattern for single-row lookups by primary key.


31-31: Verify similar member retrievals in reminder-related code

Given the failing test test_reminder_message_is_created_20_days_before_expiry_even_if_other_span_after, we should verify that similar member retrievals in reminder-related code have been updated to SQLAlchemy 2 patterns.

Also applies to: 70-70

api/src/member/member.py (1)

Line range hint 61-64: LGTM! Good error handling for PIN code validation.

The addition of specific error handling for DataError with a clear error message improves the robustness of the code.

api/src/membership/member_entity.py (2)

8-8: LGTM: SQLAlchemy 2 compatible import added.

The addition of the text import aligns with SQLAlchemy 2's recommended approach for handling raw SQL queries.


50-50: LGTM: Proper use of SQLAlchemy 2's text construct.

The migration from raw SQL string to text() construct is correctly implemented and maintains the existing functionality while adhering to SQLAlchemy 2's best practices.

api/src/shop/stripe_customer.py (1)

110-110: LGTM! Correctly updated to SQLAlchemy 2's preferred method.

The change from db_session.query(Member).get(member_id) to db_session.get(Member, member_id) aligns with SQLAlchemy 2's recommended approach for retrieving objects by primary key.

Let's verify that no old-style queries remain in this file:

✅ Verification successful

No remaining old-style SQLAlchemy queries found in this file

After reviewing the entire file content, I can confirm that there are no remaining instances of the old-style db_session.query().get() pattern. The only database retrieval operation in the file is the updated line we reviewed, which correctly uses SQLAlchemy 2's db_session.get(Member, member_id) syntax.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining old-style SQLAlchemy queries in this file
rg -U "db_session\.query\([^)]+\)\.get\(" "api/src/shop/stripe_customer.py"

Length of output: 75


Script:

#!/bin/bash
# Let's check the entire file content to verify all database query patterns
cat api/src/shop/stripe_customer.py

Length of output: 4948

api/src/shop/shop_data.py (2)

21-21: LGTM: Import addition aligns with query changes.

The addition of TransactionContent to the imports is necessary to support the explicit joined loading patterns introduced in this file.


54-54: LGTM: Query optimization using explicit joined loads.

The nested joinedload pattern is more explicit and aligns with SQLAlchemy 2's best practices. However, let's verify the query performance impact.

✅ Verification successful

Let me analyze the results and generate a final response.

Verified: Query optimization pattern is consistently applied

The codebase scan reveals that the nested joinedload pattern is already consistently used in shop_data.py and there are no conflicting eager loading patterns across the Transaction-related queries. The optimization is correctly applied where needed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for similar query patterns that might need the same optimization
# and verify there are no conflicting query patterns.

echo "Checking for similar query patterns that might need optimization..."
rg -l "query\(Transaction\)" | while read -r file; do
    echo "=== $file ==="
    rg "query\(Transaction\).+options\(" "$file" -A 2
done

echo "Checking for potential conflicting eager loading patterns..."
rg "joinedload\(Transaction" -A 2

Length of output: 2330

api/src/migrate.py (3)

10-10: LGTM: Import of SQLAlchemy text construct.

The addition of the text import is appropriate for the SQLAlchemy 2 upgrade.


Line range hint 1-141: Verify complete migration to SQLAlchemy 2 style.

Let's ensure no old-style SQLAlchemy queries remain in the codebase.

#!/bin/bash
# Check for potential old-style SQLAlchemy queries
echo "Checking for old-style SQLAlchemy patterns..."

# Look for direct string SQL execution without text()
rg "execute\(['\"]" --type py

# Look for old-style query API
rg "\.query\(" --type py

# Look for old-style filter syntax
rg "\.filter\(" --type py

29-29: ⚠️ Potential issue

Add error handling for character set conversion.

The database character set modification is a critical operation that could fail if there's existing incompatible data.

Consider adding error handling:

-session.execute(text("ALTER DATABASE makeradmin CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci"))
+try:
+    session.execute(text("ALTER DATABASE makeradmin CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci"))
+except Exception as e:
+    logger.error(f"Failed to convert database character set: {e}")
+    raise

Let's verify the current charset:

api/src/core/auth.py (1)

134-134: LGTM! SQLAlchemy 2 upgrade change looks good.

The change from query().get() to session.get() aligns with SQLAlchemy 2's recommended approach for retrieving entities by primary key.

Let's verify if there are other similar patterns in this file that need updating:

✅ Verification successful

No remaining SQLAlchemy 1.x query().get() patterns found

The search results confirm that:

  • All direct primary key retrievals use the new SQLAlchemy 2.x session.get() pattern
  • Other query operations (filter, filter_by, one, delete) are still valid in SQLAlchemy 2.x and don't require changes
  • The remaining .get() calls are dictionary lookups (SERVICE_NAMES.get, SERVICE_PERMISSIONS.get), not database operations
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for remaining query().get() patterns in the file
rg -l "query\(.*\)\.get\(" api/src/core/auth.py

Length of output: 49


Script:

#!/bin/bash
# Let's try a broader search for any remaining query() patterns
rg "query\(" api/src/core/auth.py

# Also search for other get() patterns to ensure completeness
rg "\.get\(" api/src/core/auth.py

Length of output: 1134

api/src/membership/models.py (4)

21-21: LGTM: Import path updated for SQLAlchemy 2

The import path change for declarative_base follows SQLAlchemy 2's reorganized module structure.


115-118: LGTM: Select statement updated for SQLAlchemy 2

The select statement has been correctly simplified to match SQLAlchemy 2's query style while maintaining the same functionality.


74-74: Verify the impact of cascade_backrefs=False

The addition of cascade_backrefs=False to all relationships is aligned with SQLAlchemy 2's recommendations. However, this change modifies the cascade behavior which could affect how related objects are saved or deleted.

Let's verify the existing cascade behavior in the codebase:

Please ensure to test the following scenarios:

  1. Deleting a Member with associated records
  2. Deleting a Group with members
  3. Adding/Removing members to/from groups

Consider documenting these cascade behavior changes in your migration guide.

Also applies to: 100-102, 104-106, 144-144, 167-167, 193-193, 218-218

✅ Verification successful

The previous script had syntax issues. This new script will help us understand:

  1. How deletions are performed in the codebase
  2. How relationships are modified
  3. What cascade configurations exist
  4. How session operations are handled

This will give us better context about the impact of cascade_backrefs=False.


Based on the analysis of the codebase, I can now generate the final response:

Setting cascade_backrefs=False is safe and recommended

The change to add cascade_backrefs=False to all relationships is safe and follows SQLAlchemy 2.0 best practices. The codebase evidence shows:

  1. All relationship modifications are handled explicitly through:

    • Direct db_session.add() calls for new objects
    • Explicit relationship management using append() for collections
    • Clear db_session.delete() operations for removals
  2. No code relies on implicit cascading behavior through backrefs, as all operations are explicitly managed through:

    • Transaction management in shop/transactions.py
    • Member management in membership/ modules
    • Group/permission handling in membership/models.py
  3. The change is consistently applied across all relationship definitions, maintaining a uniform cascade behavior throughout the application.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential cascade delete operations that might be affected

# Look for delete operations on these models
echo "Searching for delete operations that might be affected by cascade changes..."
rg -l "delete.*(?:Member|Group|Key|Span|Box|PhoneNumberChangeRequest)" --type py

# Look for save/add operations that might be affected
echo "Searching for save operations that might be affected by cascade changes..."
rg -l "(?:add|save).*(?:Member|Group|Key|Span|Box|PhoneNumberChangeRequest)" --type py

Length of output: 815


Script:

#!/bin/bash
# Let's try a different approach to search for cascade-related operations

# Check for delete operations
echo "Checking delete operations..."
rg "\.delete\(\)" --type py -B 2 -A 2

# Check for relationship modifications
echo -e "\nChecking relationship modifications..."
rg "(?:append|remove|add|pop)\(" --type py -B 2 -A 2

# Check for SQLAlchemy cascade configurations
echo -e "\nChecking cascade configurations..."
rg "cascade.*=.*" --type py -B 1 -A 1

# Check for session operations that might be affected
echo -e "\nChecking session operations..."
rg "session\.(add|delete|commit)" --type py -B 2 -A 2

Length of output: 129298


167-167: Investigate failing test related to Span model

The test test_reminder_message_is_created_20_days_before_expiry_even_if_other_span_after is failing. This might be related to the cascade behavior changes in the Span model's relationship with Member.

Let's analyze the test and related code:

api/src/shop/stripe_payment_intent.py (1)

117-117: LGTM! Correct migration to SQLAlchemy 2 pattern.

The change from query().get() to get() follows SQLAlchemy 2's recommended pattern while maintaining proper error handling.

api/src/test_aid/db.py (1)

21-21: LGTM! The SQLAlchemy 2.x compatibility changes look good.

The changes correctly implement the new SQLAlchemy 2.x style by:

  1. Importing the text construct
  2. Wrapping the raw SQL query with text() in get_member_number

Also applies to: 169-169

api/src/quiz/views.py (2)

8-8: LGTM: Import changes align with SQLAlchemy 2 requirements.

The addition of text import is necessary for proper raw SQL query execution in SQLAlchemy 2.


82-82: LGTM: Entity retrieval updated to SQLAlchemy 2 style.

The change to db_session.get() follows SQLAlchemy 2's recommended approach for single entity retrieval by primary key.

api/src/shop/models.py (6)

21-21: LGTM! Clean import organization.

The consolidation of SQLAlchemy ORM imports improves code organization.


171-171: Verify action status consistency.

The modification of cascade behavior between TransactionContent and TransactionAction needs careful verification to ensure action status consistency is maintained, especially for pending actions.

Let's check for any status-dependent operations:

#!/bin/bash
# Search for status-dependent operations in transaction actions
rg -B 3 -A 3 "TransactionAction.*status.*PENDING|TransactionAction.*status.*COMPLETED" --type py

151-151: Verify transaction-related cascade operations.

The modification of cascade behavior in TransactionContent could affect transaction integrity. We should ensure that transaction cleanup operations (if any) are not affected.

Let's search for transaction-related cascade operations:

#!/bin/bash
# Search for transaction cleanup or cascade operations
rg -B 5 -A 5 "Transaction.*delete|delete.*Transaction" --type py

231-231: Verify gift card operations integrity.

The modification of cascade behavior for gift cards needs verification to ensure proper handling of gift card validation and usage tracking.

Let's check for gift card validation and usage operations:

#!/bin/bash
# Search for gift card validation and status update operations
rg -B 3 -A 3 "GiftCard.*status|status.*GiftCard" --type py

277-278: Verify accounting data integrity.

The modification of cascade behavior in accounting relationships needs verification to ensure proper handling of accounting entries and cost center allocations.

Let's check for accounting-related operations:

#!/bin/bash
# Search for accounting data operations
rg -B 3 -A 3 "ProductAccountsCostCenters.*fraction|ProductAccountsCostCenters.*type" --type py

76-80: Verify cascade behavior changes.

The addition of cascade_backrefs=False is aligned with SQLAlchemy 2's recommendations, but we should verify that this doesn't break existing cascade operations, especially for product deletions that might need to cascade to related records.

Let's check for any existing cascade delete operations:

✅ Verification successful

Based on the search results, I can now generate the final response:

The cascade_backrefs=False change is safe to implement

The codebase shows that Product relationships are managed through soft deletes (deleted_at column) rather than actual database cascading deletes. The search results reveal:

  • Products use soft deletion via deleted_at column (e.g., Product.deleted_at.is_(None) in queries)
  • Test cleanup uses explicit deletion order rather than relying on cascades
  • No cascade delete configurations found in relationship definitions
  • Related models (ProductAction, ProductAccountsCostCenters) are cleaned up explicitly when needed
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for cascade delete operations involving Product model
rg -l "cascade.*delete|delete.*cascade" --type py

Length of output: 1081


Script:

#!/bin/bash
# Let's search for relationship definitions and cascade configurations in the Product model
rg -B 3 -A 3 "class Product.*:|relationship.*Product|Product.*relationship" --type py

# Also search for any delete operations or tests involving Product
rg -B 2 -A 2 "delete.*Product|Product.*delete" --type py

Length of output: 13201

api/src/shop/pay.py (1)

224-227: Verify handling of insufficient funds scenario.

The comment indicates that insufficient funds should trigger an email notification. This might be related to the failing test test_reminder_message_is_created_20_days_before_expiry_even_if_other_span_after.

Let's verify the email notification logic:

api/src/statistics/maker_statistics.py (1)

14-14: LGTM! Import aligns with SQLAlchemy 2 requirements.

The addition of text import is necessary for the raw SQL query handling in SQLAlchemy 2.

api/src/service/entity.py (2)

Line range hint 319-332: LGTM! Consistent use of Session.get() in update operation.

The migration to session.get() is correctly implemented while maintaining the existing error handling and update logic.


Line range hint 307-346: Verify impact on failing reminder message test.

While the SQLAlchemy 2.0 migration changes in this file are correct, there's a failing test related to reminder messages:

PYTEST_ADDOPTS='-k test_reminder_message_is_created_20_days_before_expiry_even_if_other_span_after' make test

Let's investigate if these entity changes might affect the reminder message functionality:

#!/bin/bash
# Search for reminder message related code that might be affected by the SQLAlchemy changes
rg -l "reminder.*message" | xargs rg "db_session\.(query|get)"

# Search for any model classes related to reminders
rg -l "class.*Reminder" | xargs cat
api/src/shop/transactions.py (2)

28-28: LGTM: SQLAlchemy 2.x compatible import added.

The addition of text import from sqlalchemy is appropriate for the SQLAlchemy 2 upgrade, as it's the recommended way to construct raw SQL queries in the new version.


185-187: LGTM: Member retrieval updated to SQLAlchemy 2.x style.

The changes correctly implement SQLAlchemy 2's simplified API for retrieving objects by primary key. The error handling is appropriate, with clear error messages when members are not found.

Also applies to: 346-348

api/src/firstrun.py (1)

137-137: LGTM! Clean upgrade to SQLAlchemy 2's simplified get syntax.

The change from db_session.query(Member).get(member_id) to db_session.get(Member, member_id) correctly implements SQLAlchemy 2's new pattern for retrieving entities by primary key. This modification:

  • Maintains the same functionality while being more concise
  • Properly integrates with the existing error handling
  • Aligns with the PR objective of upgrading to SQLAlchemy 2
api/src/shop/stripe_event.py (3)

143-146: LGTM: SQLAlchemy 2.0 compliant member retrieval

The update to use db_session.get() follows SQLAlchemy 2.0's recommended pattern for retrieving single objects by primary key. The error handling for non-existent members is properly maintained.


287-290: LGTM: SQLAlchemy 2.0 compliant member retrieval

The update to use db_session.get() is consistent with SQLAlchemy 2.0's patterns and maintains proper error handling for non-existent members.


365-368: LGTM: SQLAlchemy 2.0 compliant member retrieval with improved type annotation

The update to use db_session.get() follows SQLAlchemy 2.0's patterns. The addition of the type annotation Optional[Member] improves code clarity and type safety.

api/src/shop/stripe_subscriptions.py (2)

442-442: LGTM! Clean SQLAlchemy 2 upgrade.

The change from query(Member).get(member_id) to get(Member, member_id) correctly implements SQLAlchemy 2's simplified API for retrieving records by primary key.


496-496: LGTM! Clean SQLAlchemy 2 upgrade.

The change from query(Member).get(member_id) to get(Member, member_id) correctly implements SQLAlchemy 2's simplified API for retrieving records by primary key.

flask
flask_cors
sqlalchemy>=1.4.9,<2.0
sqlalchemy~=2.0
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the comprehensive analysis of the codebase, I can now provide a final response regarding the SQLAlchemy 2.0 migration concern:

Review SQLAlchemy 2.0 migration requirements and impact

The codebase requires significant updates to be SQLAlchemy 2.0 compatible:

  1. Current SQLAlchemy 1.x patterns found:

    • Extensive use of deprecated query() API throughout
    • Legacy session management using scoped_session and sessionmaker
    • Direct use of text() for raw SQL queries
    • Heavy use of relationship loading with joinedload()
  2. Required changes:

    • Replace query() with select() statements
    • Update session management to use 2.0-style context managers
    • Migrate raw SQL queries to use 2.0-style text() with parameters
    • Review and update relationship loading patterns
    • Add type annotations for better 2.0 compatibility
  3. Migration strategy:

    • Enable SQLAlchemy 1.x deprecation warnings
    • Incrementally update query patterns starting with core functionality
    • Test thoroughly as SQLAlchemy 2.0 has stricter typing requirements
🔗 Analysis chain

Review SQLAlchemy 2.0 migration guide for breaking changes.

Given that SQLAlchemy 2.0 introduces significant API changes and the PR comments indicate failing tests, please ensure you've reviewed the SQLAlchemy 2.0 Migration Guide. Key areas to focus on:

  • Query API changes (.query().select())
  • Session API changes
  • ORM relationship loading changes
  • Type annotation changes

Based on the search results, I can see extensive usage of SQLAlchemy 1.x style query patterns throughout the codebase. The key findings are:

  1. Heavy use of query() API which is deprecated in SQLAlchemy 2.0
  2. Extensive use of relationship loading patterns like joinedload()
  3. Session management and transaction patterns that may need updates

Let me verify if there are any SQLAlchemy 2.0 style queries already present to determine if migration is in progress.


Based on the search results, I can see that the codebase heavily uses SQLAlchemy 1.x style query patterns and needs migration to 2.0. The key findings are:

  1. Extensive use of deprecated query() API throughout the codebase
  2. Current session management using scoped_session and sessionmaker
  3. No explicit use of SQLAlchemy 2.0 style select() statements
  4. Heavy use of relationship loading with joinedload()

Let me verify if there are any SQLAlchemy 2.0 migration attempts or configuration settings.


Let me try a different search pattern to verify SQLAlchemy configuration and potential 2.0 migration attempts.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for potentially affected SQLAlchemy patterns that need migration

# Test 1: Search for old-style query patterns
echo "Checking for old-style query patterns..."
rg "\.query\(" -A 3

# Test 2: Search for session usage patterns
echo "Checking for session usage patterns..."
rg "Session|session" -A 3

# Test 3: Search for relationship loading patterns
echo "Checking for relationship loading patterns..."
rg "relationship|joinedload|subqueryload" -A 3

Length of output: 227929


Script:

#!/bin/bash
# Search for SQLAlchemy 2.0 style select() patterns
rg "select\(\)" -A 3

# Search for Session configuration to check SQLAlchemy version settings
rg "create_engine|Session" -B 2 -A 2

Length of output: 142951


Script:

#!/bin/bash
# Search for SQLAlchemy configuration and engine setup
rg "sqlalchemy\.(url|future|engine|create_engine)" -A 3

# Search for any SQLAlchemy 2.0 style select() statements
rg "select\(\)" -A 3 --type py

Length of output: 165


Script:

#!/bin/bash
# Search for SQLAlchemy imports and configuration
rg "from sqlalchemy" -A 2

# Search for SQLAlchemy engine configuration
rg "engine.*=.*create_engine" -A 2

# Search for any SQLAlchemy 2.0 style operations
rg "with.*Session" -A 2

Length of output: 8419

@emanuelen5
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2024

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@emanuelen5 emanuelen5 force-pushed the sqlalchemy-2-upgrade branch 2 times, most recently from 0e9d5e6 to d92277a Compare December 29, 2024 16:33
@emanuelen5 emanuelen5 merged commit 9e1ab35 into main Dec 29, 2024
12 of 13 checks passed
@emanuelen5 emanuelen5 deleted the sqlalchemy-2-upgrade branch December 29, 2024 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade to sqlalchemy 2

4 participants