From: Mike Bayer Date: Thu, 22 Nov 2018 15:39:29 +0000 (-0500) Subject: Add __clause_element__ to ColumnProperty X-Git-Tag: rel_1_3_0b2~79^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=835444be72bb595b1ed3ee5458a86202813412a6;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add __clause_element__ to ColumnProperty 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``. Fixes: #4372 Change-Id: I5d3d1adb9c77de0566298bc2c46e9001d314b0c7 --- diff --git a/doc/build/changelog/unreleased_13/4372.rst b/doc/build/changelog/unreleased_13/4372.rst new file mode 100644 index 0000000000..60ecf114d5 --- /dev/null +++ b/doc/build/changelog/unreleased_13/4372.rst @@ -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``. diff --git a/doc/build/errors.rst b/doc/build/errors.rst index a81f509a83..af2fe80e31 100644 --- a/doc/build/errors.rst +++ b/doc/build/errors.rst @@ -318,6 +318,50 @@ the database driver (DBAPI), not SQLAlchemy itself. SQL Expression Language ======================= +TypeError: not supported between instances of 'ColumnProperty' and +----------------------------------------------------------------------------------------- + +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 diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 360edc6e9b..01ce7043a0 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -163,6 +163,13 @@ class ColumnProperty(StrategizedProperty): 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. diff --git a/test/ext/declarative/test_basic.py b/test/ext/declarative/test_basic.py index d2dc1a4250..68aa9b1f55 100644 --- a/test/ext/declarative/test_basic.py +++ b/test/ext/declarative/test_basic.py @@ -7,11 +7,11 @@ from sqlalchemy import exc 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 @@ -207,6 +207,53 @@ class DeclarativeTest(DeclarativeTestBase): 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'