]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add __clause_element__ to ColumnProperty
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 22 Nov 2018 15:39:29 +0000 (10:39 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 28 Nov 2018 01:12:16 +0000 (20:12 -0500)
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

doc/build/changelog/unreleased_13/4372.rst [new file with mode: 0644]
doc/build/errors.rst
lib/sqlalchemy/orm/properties.py
test/ext/declarative/test_basic.py

diff --git a/doc/build/changelog/unreleased_13/4372.rst b/doc/build/changelog/unreleased_13/4372.rst
new file mode 100644 (file)
index 0000000..60ecf11
--- /dev/null
@@ -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``.
index a81f509a839bfefd986c1046bc14cabecc0edb17..af2fe80e3186fa23c914b571184c92a0cd801d21 100644 (file)
@@ -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
index 360edc6e9be70145f104942e0db4833ac2e071b2..01ce7043a0ae736e5798e93f8218bebb01329bb7 100644 (file)
@@ -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.
index d2dc1a4250094691a4638cd98008c772d4f23e27..68aa9b1f55395649c48b5326752d396d54005d00 100644 (file)
@@ -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'