row_processor is None when querying with hybrid_property

Issue #3448 resolved
First Last created an issue

I'm using a hybrid_property to map a column containing int identifiers to the un-persisted python objects they identify. We're writing queries that compare that column with those python objects. In this example, I'm mapping an int "id" column to a python IdObject wrapper.

In SQLAlchemy 0.9.9, this example ran with 1 row being returned, but in SQLAlchemy 1.0.5, I get a TypeError (traceback pasted below). Is there a better (working) way to do this?

from sqlalchemy import Table, Column, Integer, MetaData, create_engine
from sqlalchemy.ext.hybrid import hybrid_property, Comparator
from sqlalchemy.orm import mapper, Query, Session


class Entity(object):
    pass


class IdObject(object):
    def __init__(self, id_):
        self.id = id_

    def __cmp__(self, other):
        if isinstance(other, int):
            return cmp(self.id, other)
        return cmp(self.id, other.id)


class IdObjComparator(Comparator):
    def __init__(self, id_):
        if isinstance(id_, (IdObject, IdObjComparator)):
            self.id = id_.id
        else:
            self.id = id_

    def operate(self, op, *other, **kwargs):
        return op(self.id,
                  *(IdObjComparator(id_).id
                    for id_ in other),
                  **kwargs)

    def __clause_element__(self):
        return self.id


def test_hybrid_property(url):
    engine = create_engine(url)
    metadata = MetaData(bind=engine)
    table = Table('test_tbl', metadata,
                  Column('id', Integer, primary_key=True),
                  Column('value', Integer))

    metadata.create_all()

    engine.execute(table.insert().values([{'value': 1}]))

    mapper(Entity,
           table,
           properties={'_id': table.c.id},
           include_properties=())

    hybrid_id = hybrid_property(
        lambda zelf: IdObject(zelf._id)
    ).comparator(lambda klass: IdObjComparator(klass._id))

    Entity.id = hybrid_id

    query_for_id1 = (Query([Entity.id])
                     .filter(Entity.id == IdObject(1))
                     .order_by(Entity.id))

    results = list(query_for_id1.with_session(Session(bind=engine)))
    assert len(results) == 1


if __name__ == '__main__':
    test_hybrid_property('postgresql://rich@/test_hybrid_prop')
Traceback (most recent call last):
  File "/Users/rich/Dev/test/test/test_hybrid_property.py", line 63, in <module>
    test_hybrid_property('postgresql://rich@/test_hybrid_prop')
  File "/Users/rich/Dev/test/test/test_hybrid_property.py", line 58, in test_hybrid_property
    results = list(query_for_id1.with_session(Session(bind=engine)))
  File "/Users/rich/.virtualenvs/test/lib/python2.7/site-packages/sqlalchemy/orm/loading.py", line 84, in instances
    util.raise_from_cause(err)
  File "/Users/rich/.virtualenvs/test/lib/python2.7/site-packages/sqlalchemy/util/compat.py", line 199, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb)
  File "/Users/rich/.virtualenvs/test/lib/python2.7/site-packages/sqlalchemy/orm/loading.py", line 72, in instances
    for row in fetch]
TypeError: 'NoneType' object is not callable

Comments (5)

  1. Mike Bayer repo owner

    __clause_element__() has to return a ColumnElement, not the mapped attribute. so when you say "cls.id", that's the InstrumentedAttribute, not the SQL element. change as follows and works in both cases:

    class IdObjComparator(Comparator):
        def __init__(self, id_):
            if isinstance(id_, (IdObject, IdObjComparator)):
                self.id = id_.id
            else:
                self.id = id_
    
        def operate(self, op, *other, **kwargs):
            return op(self.id,
                      *(IdObjComparator(id_).id
                        for id_ in other),
                      **kwargs)
    
        def __clause_element__(self):
            return self.id.__clause_element__()
    
  2. Mike Bayer repo owner
    • Fixed an unexpected-use regression whereby custom :class:.Comparator objects that made use of the __clause_element__() method and returned an object that was an ORM-mapped :class:.InstrumentedAttribute and not explicitly a :class:.ColumnElement would fail to be correctly handled when passed as an expression to :meth:.Session.query. The logic in 0.9 happened to succeed on this, so this use case is now supported. fixes #3448

    → <<cset e765c55e8cc7>>

  3. Log in to comment