annotated_classes = {}
-def _deep_annotate(element, annotations, exclude=None):
+def _deep_annotate(
+ element, annotations, exclude=None, detect_subquery_cols=False
+):
"""Deep copy the given ClauseElement, annotating each element
with the given annotations dictionary.
cloned_ids = {}
def clone(elem, **kw):
+ kw["detect_subquery_cols"] = detect_subquery_cols
id_ = id(elem)
if id_ in cloned_ids:
and hasattr(elem, "proxy_set")
and elem.proxy_set.intersection(exclude)
):
- newelem = elem._clone(**kw)
+ newelem = elem._clone(clone=clone, **kw)
elif annotations != elem._annotations:
- newelem = elem._annotate(annotations)
+ if detect_subquery_cols and elem._is_immutable:
+ newelem = elem._clone(clone=clone, **kw)._annotate(annotations)
+ else:
+ newelem = elem._annotate(annotations)
else:
newelem = elem
newelem._copy_internals(clone=clone)
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import event
+from sqlalchemy import exc as sa_exc
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy.orm import with_loader_criteria
from sqlalchemy.orm.decl_api import declared_attr
from sqlalchemy.testing import eq_
+from sqlalchemy.testing.assertions import expect_raises
from sqlalchemy.testing.assertsql import CompiledSQL
from sqlalchemy.testing.fixtures import fixture_session
from sqlalchemy.testing.util import resolve_lambda
"WHERE users.name != :name_1",
)
+ def test_with_loader_criteria_recursion_check_scalar_subq(
+ self, user_address_fixture
+ ):
+ """test #7491"""
+
+ User, Address = user_address_fixture
+ subq = select(Address).where(Address.id == 8).scalar_subquery()
+ stmt = (
+ select(User)
+ .join(Address)
+ .options(with_loader_criteria(Address, Address.id == subq))
+ )
+ self.assert_compile(
+ stmt,
+ "SELECT users.id, users.name FROM users JOIN addresses "
+ "ON users.id = addresses.user_id AND addresses.id = "
+ "(SELECT addresses.id, addresses.user_id, "
+ "addresses.email_address FROM addresses "
+ "WHERE addresses.id = :id_1)",
+ )
+
+ def test_with_loader_criteria_recursion_check_from_subq(
+ self, user_address_fixture
+ ):
+ """test #7491"""
+
+ User, Address = user_address_fixture
+ subq = select(Address).where(Address.id == 8).subquery()
+ stmt = (
+ select(User)
+ .join(Address)
+ .options(with_loader_criteria(Address, Address.id == subq.c.id))
+ )
+ # note this query is incorrect SQL right now. This is a current
+ # artifact of how with_loader_criteria() is used and may be considered
+ # a bug at some point, in which case if fixed this query can be
+ # changed. the main thing we are testing at the moment is that
+ # there is not a recursion overflow.
+ self.assert_compile(
+ stmt,
+ "SELECT users.id, users.name FROM users JOIN addresses "
+ "ON users.id = addresses.user_id AND addresses.id = anon_1.id",
+ )
+
def test_select_mapper_columns_mapper_criteria(self, user_address_fixture):
User, Address = user_address_fixture
),
)
+ @testing.combinations(
+ (joinedload, False),
+ (lazyload, True),
+ (subqueryload, False),
+ (selectinload, True),
+ argnames="opt,results_supported",
+ )
+ def test_loader_criteria_subquery_w_same_entity(
+ self, user_address_fixture, opt, results_supported
+ ):
+ """test #7491.
+
+ note this test also uses the not-quite-supported form of subquery
+ criteria introduced by #7489. where we also have to clone
+ the subquery linked only from a column criteria. this required
+ additional changes to the _annotate() method that is also
+ test here, which is why two of the loader strategies still fail;
+ we're just testing that there's no recursion overflow with this
+ very particular form.
+
+ """
+ User, Address = user_address_fixture
+
+ s = Session(testing.db, future=True)
+
+ def go(value):
+ subq = (
+ select(Address.id)
+ .where(Address.email_address != value)
+ .subquery()
+ )
+ stmt = (
+ select(User)
+ .options(
+ # subquery here would need to be added to the FROM
+ # clause. this isn't quite supported and won't work
+ # right now with joinedoad() or subqueryload().
+ opt(User.addresses.and_(Address.id == subq.c.id)),
+ )
+ .order_by(User.id)
+ )
+ result = s.execute(stmt)
+ return result
+
+ for value in (
+ "ed@wood.com",
+ "ed@lala.com",
+ "ed@wood.com",
+ "ed@lala.com",
+ ):
+ s.close()
+
+ if not results_supported:
+ # for joinedload and subqueryload, the query generated here
+ # is invalid right now; this is because it's already not
+ # quite a supported pattern to refer to a subquery-bound
+ # column in loader criteria. However, the main thing we want
+ # to prevent here is the recursion overflow, so make sure
+ # we get a DBAPI error at least indicating compilation
+ # succeeded.
+ with expect_raises(sa_exc.DBAPIError):
+ go(value).scalars().unique().all()
+ else:
+ result = go(value).scalars().unique().all()
+
+ eq_(
+ result,
+ self._user_minus_edwood(*user_address_fixture)
+ if value == "ed@wood.com"
+ else self._user_minus_edlala(*user_address_fixture),
+ )
+
@testing.combinations((True,), (False,), argnames="use_compiled_cache")
def test_selectinload_nested_criteria(
self, user_order_item_fixture, use_compiled_cache