Skip to content

Bug: Still errors in SQLAlchemyRepository update method with lazy #552

@Glinte

Description

@Glinte

Description

#533 which fixes #524 is insufficient, because (if I understand correctly) getattr(data, relationship.key, MISSING) is done before the check on relationship.lazy. If lazy=raise/raise_on_sql it dies immediately.

https://github.com/litestar-org/advanced-alchemy/pull/533/files#diff-a8a496590f46ff61fdd4d9f2b6521629905ced8b68a45770d068b5f86b825bbdR1509-R1519

Image

URL to code causing the issue

No response

MCVE

#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "sqlalchemy>=2.0",
#   "advanced-alchemy",
# ]
# ///
from advanced_alchemy.base import UUIDBase
from sqlalchemy import ForeignKey, String, create_engine
from sqlalchemy.orm import Mapped, mapped_column, relationship, sessionmaker

from advanced_alchemy.repository import SQLAlchemySyncRepository


class Author(UUIDBase):
    __tablename__ = "author"
    name: Mapped[str] = mapped_column(String(100))
    books: Mapped[list["Book"]] = relationship(back_populates="author", lazy="raise")


class Book(UUIDBase):
    __tablename__ = "book"
    title: Mapped[str] = mapped_column(String(200))
    author_id: Mapped[int] = mapped_column(ForeignKey("author.id"))
    author: Mapped[Author] = relationship(back_populates="books", lazy="raise")


class AuthorRepo(SQLAlchemySyncRepository[Author]):
    model_type = Author


def update_triggers_lazy_raise_on_getattr():
    engine = create_engine("sqlite:///:memory:")
    session_factory = sessionmaker(engine)

    with engine.begin() as conn:
        UUIDBase.metadata.create_all(conn)

    with session_factory() as session:
        # Seed an author with a related book
        author = Author(name="Original", books=[Book(title="B1")])
        session.add(author)
        session.commit()

        session.refresh(author)
        author_id = author.id

    with session_factory() as session:
        # Prepare repo
        repo = AuthorRepo(session=session)
        # Get a persisted author instance that will raise if .books is accessed
        author = repo.get(author_id)
        author.name = "Updated"

        repo.update(author)
        # Traceback (most recent call last):
        #   File "...\advanced_alchemy\exceptions.py", line 286, in wrap_sqlalchemy_exception
        #     yield
        #   File "...\advanced_alchemy\repository\_sync.py", line 1511, in update
        #     if (new_value := getattr(data, relationship.key, MISSING)) is not MISSING:
        #                      ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        #   File "...\sqlalchemy\orm\attributes.py", line 569, in __get__
        #     return self.impl.get(state, dict_)  # type: ignore[no-any-return]
        #            ~~~~~~~~~~~~~^^^^^^^^^^^^^^
        #   File "...\sqlalchemy\orm\attributes.py", line 1096, in get
        #     value = self._fire_loader_callables(state, key, passive)
        #   File "...\sqlalchemy\orm\attributes.py", line 1131, in _fire_loader_callables
        #     return self.callable_(state, passive)
        #            ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
        #   File "...\sqlalchemy\orm\strategies.py", line 915, in _load_for_state
        #     self._invoke_raise_load(state, passive, "raise")
        #     ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
        #   File "...\sqlalchemy\orm\strategies.py", line 867, in _invoke_raise_load
        #     raise sa_exc.InvalidRequestError(
        #         "'%s' is not available due to lazy='%s'" % (self, lazy)
        #     )
        # sqlalchemy.exc.InvalidRequestError: 'Author.books' is not available due to lazy='raise'

if __name__ == "__main__":
    update_triggers_lazy_raise_on_getattr()

Steps to reproduce

Run this file.

Screenshots

No response

Logs

Traceback (most recent call last):
  File "...\advanced_alchemy\exceptions.py", line 286, in wrap_sqlalchemy_exception
    yield
  File "...\advanced_alchemy\repository\_sync.py", line 1511, in update
    if (new_value := getattr(data, relationship.key, MISSING)) is not MISSING:
                     ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\sqlalchemy\orm\attributes.py", line 569, in __get__
    return self.impl.get(state, dict_)  # type: ignore[no-any-return]
           ~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "...\sqlalchemy\orm\attributes.py", line 1096, in get
    value = self._fire_loader_callables(state, key, passive)
  File "...\sqlalchemy\orm\attributes.py", line 1131, in _fire_loader_callables
    return self.callable_(state, passive)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "...\sqlalchemy\orm\strategies.py", line 915, in _load_for_state
    self._invoke_raise_load(state, passive, "raise")
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\sqlalchemy\orm\strategies.py", line 867, in _invoke_raise_load
    raise sa_exc.InvalidRequestError(
        "'%s' is not available due to lazy='%s'" % (self, lazy)
    )
sqlalchemy.exc.InvalidRequestError: 'Author.books' is not available due to lazy='raise'

Package Version

Python 3.13.7
dvanced-alchemy==1.6.3
sqlalchemy==2.0.43

Platform

  • Linux
  • Mac
  • Windows
  • Other (Please specify in the description above)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions