Issue 1:
Migrated issue, originally created by Michael Bayer (@zzzeek)

because we don't include the parentmapper annotation in the _optimized_compare():

from sqlalchemy import Column, Integer, Text, ForeignKey, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, relationship

Base = declarative_base()

class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)

class Child(Base):
__tablename__ = "child"
_id_parent = Column(
"id_parent", Integer, ForeignKey(Parent.id), primary_key=True)
name = Column(Text, primary_key=True)
parent = relationship(Parent)

engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
session = Session(engine)

p = Parent(id=1)
session.add(p)
session.commit()

c = Child(name="foo", parent=p)
session.add(c)
session.commit()

session.query(Child).filter(Child.parent == p).delete("evaluate")

There are two approaches.

One is that we look into getting parentmapper back into PJ/SJ. This was removed a long time ago but we might want to revisit this assumption as mechanics have changed a lot. It would be nice if these annotations were set up at relationship startup time, rather than having to re-inject when we build up relationship comparator operations.

The other is probably what we should do here and is just search for the column in the local mapper. We also shouldn't assume ".key" on the clause itself is important, there don't seem to be tests for that case and it should probably raise, so this needs a lot more tests:

diff --git a/lib/sqlalchemy/orm/evaluator.py b/lib/sqlalchemy/orm/evaluator.py
index 1e828ff..814b28f 100644
--- a/lib/sqlalchemy/orm/evaluator.py
+++ b/lib/sqlalchemy/orm/evaluator.py
@@ -7,6 +7,7 @@

import operator
from ..sql import operators
+from .. import inspect

class UnevaluatableError(Exception):
@@ -28,6 +29,7 @@ _notimplemented_ops = set(getattr(operators, op)
class EvaluatorCompiler(object):
def __init__(self, target_cls=None):
self.target_cls = target_cls
+ self.mapper = inspect(target_cls) if target_cls is not None else None

def process(self, clause):
meth = getattr(self, "visit_%s" % clause.__visit_name__, None)
@@ -58,8 +60,12 @@ class EvaluatorCompiler(object):
parentmapper.class_
)
key = parentmapper._columntoproperty[clause].key
+ elif self.mapper and clause in self.mapper._columntoproperty:
+ key = self.mapper._columntoproperty[clause].key
else:
- key = clause.key
+ raise UnevaluatableError(
+ "Can't evaluate column: %s" % clause
+ )

get_corresponding_attr = operator.attrgetter(key)
return lambda obj: get_corresponding_attr(obj)
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index b649c9e..7ac0ab5 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -1372,6 +1372,7 @@ class RelationshipProperty(StrategizedProperty):

if adapt_source:
criterion = adapt_source(criterion)
+
return criterion

def _lazy_none_clause(self, reverse_direction=False, adapt_source=None):

Issue 2:
Migrated issue, originally created by Michael Bayer (@zzzeek)

this is related to #3365 but is a more fundamental change.

First, test case from #3365:

from sqlalchemy import Column, Integer, Text, ForeignKey, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, relationship

Base = declarative_base()

class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)

class Child(Base):
__tablename__ = "child"
_id_parent = Column(
"id_parent", Integer, ForeignKey(Parent.id), primary_key=True)
name = Column(Text, primary_key=True)
parent = relationship(Parent)

engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
session = Session(engine)

p = Parent(id=1)
session.add(p)
session.commit()

c = Child(name="foo", parent=p)
session.add(c)
session.commit()

session.query(Child).filter(Child.parent == p).delete("evaluate")

raises: "AttributeError: 'Child' object has no attribute 'id_parent'"

with the fix, raises: "sqlalchemy.exc.InvalidRequestError: Could not evaluate current criteria in Python. Specify 'fetch' or False for the synchronize_session parameter."

the patch involves adding ORM context to the primaryjoin/secondaryjoin:

diff --git a/lib/sqlalchemy/orm/evaluator.py b/lib/sqlalchemy/orm/evaluator.py
index 1e828ff..fb59186 100644
--- a/lib/sqlalchemy/orm/evaluator.py
+++ b/lib/sqlalchemy/orm/evaluator.py
@@ -59,7 +59,9 @@ class EvaluatorCompiler(object):
)
key = parentmapper._columntoproperty[clause].key
else:
- key = clause.key
+ raise UnevaluatableError(
+ "Cannot evaluate column: %s" % clause
+ )

get_corresponding_attr = operator.attrgetter(key)
return lambda obj: get_corresponding_attr(obj)
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index b649c9e..291af09 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -1942,6 +1942,7 @@ class JoinCondition(object):
self._annotate_fks()
self._annotate_remote()
self._annotate_local()
+ self._annotate_parentmapper()
self._setup_pairs()
self._check_foreign_cols(self.primaryjoin, True)
if self.secondaryjoin is not None:
@@ -2405,6 +2406,19 @@ class JoinCondition(object):
self.primaryjoin, {}, locals_
)

+ def _annotate_parentmapper(self):
+ if self.prop is None:
+ return
+
+ def parentmappers_(elem):
+ if "remote" in elem._annotations:
+ return elem._annotate({"parentmapper": self.prop.mapper})
+ elif "local" in elem._annotations:
+ return elem._annotate({"parentmapper": self.prop.parent})
+ self.primaryjoin = visitors.replacement_traverse(
+ self.primaryjoin, {}, parentmappers_
+ )
+
def _check_remote_side(self):
if not self.local_remote_pairs:
raise sa_exc.ArgumentError(
@@ -2811,9 +2825,6 @@ class JoinCondition(object):

bind_to_col = dict((binds[col].key, col) for col in binds)

- # this is probably not necessary
- lazywhere = _deep_deannotate(lazywhere)
-
return lazywhere, bind_to_col, equated_columns

We'd just start tracking "parentmapper" throughout all PJ/SJ conditions.