Skip to content

Commit

Permalink
Merge "Add __clause_element__ to ColumnProperty"
Browse files Browse the repository at this point in the history
  • Loading branch information
zzzeek authored and Gerrit Code Review committed Nov 30, 2018
2 parents f916fa3 + 835444b commit 7940e7d
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 2 deletions.
10 changes: 10 additions & 0 deletions doc/build/changelog/unreleased_13/4372.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. change::
:tags: bug, orm declarative
:tickets: 4372

Added a ``__clause_element__()`` method to :class:`.ColumnProperty` which
can allow the usage of a not-fully-declared column or deferred attribute in
a declarative mapped class slightly more friendly when it's used in a
constraint or other column-oriented scenario within the class declaration,
though this still can't work in open-ended expressions; prefer to call the
:attr:`.ColumnProperty.expression` attribute if receiving ``TypeError``.
44 changes: 44 additions & 0 deletions doc/build/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,50 @@ the database driver (DBAPI), not SQLAlchemy itself.
SQL Expression Language
=======================

TypeError: <operator> not supported between instances of 'ColumnProperty' and <something>
-----------------------------------------------------------------------------------------

This often occurs when attempting to use a :func:`.column_property` or
:func:`.deferred` object in the context of a SQL expression, usually within
declarative such as::

class Bar(Base):
__tablename__ = 'bar'

id = Column(Integer, primary_key=True)
cprop = deferred(Column(Integer))

__table_args__ = (
CheckConstraint(cprop > 5),
)

Above, the ``cprop`` attribute is used inline before it has been mapped,
however this ``cprop`` attribute is not a :class:`.Column`,
it's a :class:`.ColumnProperty`, which is an interim object and therefore
does not have the full functionality of either the :class:`.Column` object
or the :class:`.InstrmentedAttribute` object that will be mapped onto the
``Bar`` class once the declarative process is complete.

While the :class:`.ColumnProperty` does have a ``__clause_element__()`` method,
which allows it to work in some column-oriented contexts, it can't work in an
open-ended comparison context as illustrated above, since it has no Python
``__eq__()`` method that would allow it to interpret the comparison to the
number "5" as a SQL expression and not a regular Python comparison.

The solution is to access the :class:`.Column` directly using the
:attr:`.ColumnProperty.expression` attribute::

class Bar(Base):
__tablename__ = 'bar'

id = Column(Integer, primary_key=True)
cprop = deferred(Column(Integer))

__table_args__ = (
CheckConstraint(cprop.expression > 5),
)


.. _error_2afi:

This Compiled object is not bound to any Engine or Connection
Expand Down
7 changes: 7 additions & 0 deletions lib/sqlalchemy/orm/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ def _memoized_attr__deferred_column_loader(self, state, strategies):
self.parent.class_manager,
strategies.LoadDeferredColumns(self.key), self.key)

def __clause_element__(self):
"""Allow the ColumnProperty to work in expression before it is turned
into an instrumented attribute.
"""

return self.expression

@property
def expression(self):
"""Return the primary column or expression for this ColumnProperty.
Expand Down
51 changes: 49 additions & 2 deletions test/ext/declarative/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import sqlalchemy as sa
from sqlalchemy import testing, util
from sqlalchemy import MetaData, Integer, String, ForeignKey, \
ForeignKeyConstraint, Index
ForeignKeyConstraint, Index, UniqueConstraint, CheckConstraint
from sqlalchemy.testing.schema import Table, Column
from sqlalchemy.orm import relationship, create_session, class_mapper, \
joinedload, configure_mappers, backref, clear_mappers, \
column_property, composite, Session, properties
column_property, composite, Session, properties, deferred
from sqlalchemy.util import with_metaclass
from sqlalchemy.ext.declarative import declared_attr, synonym_for
from sqlalchemy.testing import fixtures, mock
Expand Down Expand Up @@ -264,6 +264,53 @@ class Foo(Base):
go
)

def test_using_explicit_prop_in_schema_objects(self):
class Foo(Base):
__tablename__ = 'foo'

id = Column(Integer, primary_key=True)
cprop = column_property(Column(Integer))

__table_args__ = (
UniqueConstraint(cprop),
)
uq = [
c for c in Foo.__table__.constraints
if isinstance(c, UniqueConstraint)][0]
is_(uq.columns.cprop, Foo.__table__.c.cprop)

class Bar(Base):
__tablename__ = 'bar'

id = Column(Integer, primary_key=True)
cprop = deferred(Column(Integer))

__table_args__ = (
CheckConstraint(cprop > sa.func.foo()),
)
ck = [
c for c in Bar.__table__.constraints
if isinstance(c, CheckConstraint)][0]
is_(ck.columns.cprop, Bar.__table__.c.cprop)

if testing.requires.python3.enabled:
# test the existing failure case in case something changes
def go():
class Bat(Base):
__tablename__ = 'bat'

id = Column(Integer, primary_key=True)
cprop = deferred(Column(Integer))

# we still can't do an expression like
# "cprop > 5" because the column property isn't
# a full blown column

__table_args__ = (
CheckConstraint(cprop > 5),
)
assert_raises(TypeError, go)

def test_relationship_level_msg_for_invalid_callable(self):
class A(Base):
__tablename__ = 'a'
Expand Down

0 comments on commit 7940e7d

Please sign in to comment.