evaluator can't locate ORM entity when using a many-to-one comparison

Issue #3365 duplicate
Mike Bayer repo owner created an issue

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):

Comments (3)

  1. Mike Bayer reporter

    one place this will break right now is if the target_cls is a joined inh base class and the incoming element is against a subclass. We'd have to expand upon the _columntoproperty lookup to include a full collection based on _self_and_descendants. The change proposed in #3366 would make this unnecessary, however changing it here is localized and safer for a 1.0 and even 0.9 backport.

  2. Mike Bayer reporter
    • changed milestone to 1.1

    targeting this at 1.1 for now so that it's easier to keep track of with potential for a 1.0 backport.

  3. Log in to comment