]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added a new parameter :paramref:`.Operators.op.is_comparison`. This
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 1 Feb 2014 00:14:08 +0000 (19:14 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 1 Feb 2014 00:14:08 +0000 (19:14 -0500)
flag allows a custom op from :meth:`.Operators.op` to be considered
as a "comparison" operator, thus usable for custom
:paramref:`.relationship.primaryjoin` conditions.

doc/build/changelog/changelog_09.rst
doc/build/orm/relationships.rst
lib/sqlalchemy/sql/operators.py
test/orm/test_relationships.py
test/sql/test_operators.py

index fbb6db335595c93ccb690ea7342cb7bac24fce42..4a4ec10089a253c8ab06ff50b2422967295314c1 100644 (file)
 .. changelog::
     :version: 0.9.2
 
+    .. change::
+        :tags: feature, orm
+
+        Added a new parameter :paramref:`.Operators.op.is_comparison`.  This
+        flag allows a custom op from :meth:`.Operators.op` to be considered
+        as a "comparison" operator, thus usable for custom
+        :paramref:`.relationship.primaryjoin` conditions.
+
+        .. seealso::
+
+            :ref:`relationship_custom_operator`
+
+
     .. change::
         :tags: bug, sqlite
 
index 98a6e1becba2f6e510ca934b49488dde8ffef141..238493faa174b106f0100afe6f3eaa0adae85d5d 100644 (file)
@@ -1077,6 +1077,60 @@ of these features on its own::
                         )
 
 
+.. _relationship_custom_operator:
+
+Using custom operators in join conditions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another use case for relationships is the use of custom operators, such
+as Postgresql's "is contained within" ``<<`` operator when joining with
+types such as :class:`.postgresql.INET` and :class:`.postgresql.CIDR`.
+For custom operators we use the :meth:`.Operators.op` function::
+
+    inet_column.op("<<")(cidr_column)
+
+However, if we construct a :paramref:`.relationship.primaryjoin` using this
+operator, :func:`.relationship` will still need more information.  This is because
+when it examines our primaryjoin condition, it specifically looks for operators
+used for **comparisons**, and this is typically a fixed list containing known
+comparison operators such as ``==``, ``<``, etc.   So for our custom operator
+to participate in this system, we need it to register as a comparison operator
+using the :paramref:`.Operators.op.is_comparison` parameter::
+
+    inet_column.op("<<", is_comparison=True)(cidr_column)
+
+A complete example::
+
+    class IPA(Base):
+        __tablename__ = 'ip_address'
+
+        id = Column(Integer, primary_key=True)
+        v4address = Column(INET)
+
+        network = relationship("Network",
+                            primaryjoin="IPA.v4address.op('<<', is_comparison=True)"
+                                "(foreign(Network.v4representation))",
+                            viewonly=True
+                        )
+    class Network(Base):
+        __tablename__ = 'network'
+
+        id = Column(Integer, primary_key=True)
+        v4representation = Column(CIDR)
+
+Above, a query such as::
+
+    session.query(IPA).join(IPA.network)
+
+Will render as::
+
+    SELECT ip_address.id AS ip_address_id, ip_address.v4address AS ip_address_v4address
+    FROM ip_address JOIN network ON ip_address.v4address << network.v4representation
+
+.. versionadded:: 0.9.2 - Added the :paramref:`.Operators.op.is_comparison`
+   flag to assist in the creation of :func:`.relationship` constructs using
+   custom operators.
+
 .. _self_referential_many_to_many:
 
 Self-Referential Many-to-Many Relationship
index d7ec977aa3b41a9d5891255f593ecc6d43754851..91301c78cfec2eee171682e225e83540c4a78df0 100644 (file)
@@ -102,7 +102,7 @@ class Operators(object):
         """
         return self.operate(inv)
 
-    def op(self, opstring, precedence=0):
+    def op(self, opstring, precedence=0, is_comparison=False):
         """produce a generic operator function.
 
         e.g.::
@@ -134,12 +134,23 @@ class Operators(object):
 
          .. versionadded:: 0.8 - added the 'precedence' argument.
 
+        :param is_comparison: if True, the operator will be considered as a
+         "comparison" operator, that is which evaulates to a boolean true/false
+         value, like ``==``, ``>``, etc.  This flag should be set so that
+         ORM relationships can establish that the operator is a comparison
+         operator when used in a custom join condition.
+
+         .. versionadded:: 0.9.2 - added the :paramref:`.Operators.op.is_comparison`
+            flag.
+
         .. seealso::
 
             :ref:`types_operators`
 
+            :ref:`relationship_custom_operator`
+
         """
-        operator = custom_op(opstring, precedence)
+        operator = custom_op(opstring, precedence, is_comparison)
 
         def against(other):
             return operator(self, other)
@@ -200,9 +211,10 @@ class custom_op(object):
     """
     __name__ = 'custom_op'
 
-    def __init__(self, opstring, precedence=0):
+    def __init__(self, opstring, precedence=0, is_comparison=False):
         self.opstring = opstring
         self.precedence = precedence
+        self.is_comparison = is_comparison
 
     def __eq__(self, other):
         return isinstance(other, custom_op) and \
@@ -769,7 +781,8 @@ _comparison = set([eq, ne, lt, gt, ge, le, between_op])
 
 
 def is_comparison(op):
-    return op in _comparison
+    return op in _comparison or \
+        isinstance(op, custom_op) and op.is_comparison
 
 
 def is_commutative(op):
index 8f7e2bd5576d377cb4341ea330c5dd6d1eb9a30d..ccd54284acc66bf0d334c763c3ab877ac6dec6db 100644 (file)
@@ -1517,6 +1517,45 @@ class TypedAssociationTable(fixtures.MappedTest):
 
         assert t3.count().scalar() == 1
 
+class CustomOperatorTest(fixtures.MappedTest, AssertsCompiledSQL):
+    """test op() in conjunction with join conditions"""
+
+    run_create_tables = run_deletes = None
+
+    __dialect__ = 'default'
+
+    @classmethod
+    def define_tables(cls, metadata):
+        Table('a', metadata,
+                Column('id', Integer, primary_key=True),
+                Column('foo', String(50))
+            )
+        Table('b', metadata,
+                Column('id', Integer, primary_key=True),
+                Column('foo', String(50))
+            )
+
+    def test_join_on_custom_op(self):
+        class A(fixtures.BasicEntity):
+            pass
+        class B(fixtures.BasicEntity):
+            pass
+
+        mapper(A, self.tables.a, properties={
+                'bs': relationship(B,
+                                primaryjoin=self.tables.a.c.foo.op(
+                                                '&*', is_comparison=True
+                                            )(foreign(self.tables.b.c.foo)),
+                                viewonly=True
+                                )
+            })
+        mapper(B, self.tables.b)
+        self.assert_compile(
+            Session().query(A).join(A.bs),
+            "SELECT a.id AS a_id, a.foo AS a_foo FROM a JOIN b ON a.foo &* b.foo"
+        )
+
+
 class ViewOnlyHistoryTest(fixtures.MappedTest):
     @classmethod
     def define_tables(cls, metadata):
index 670d088d2353e5273d61c8bf6318a66c8e5d03e2..79b0a717b4c0ef745023011d25c1194bfe80c016 100644 (file)
@@ -1585,3 +1585,13 @@ class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             dialect=mysql.dialect()
         )
 
+class CustomOpTest(fixtures.TestBase):
+    def test_is_comparison(self):
+        c = column('x')
+        c2 = column('y')
+        op1 = c.op('$', is_comparison=True)(c2).operator
+        op2 = c.op('$', is_comparison=False)(c2).operator
+
+        assert operators.is_comparison(op1)
+        assert not operators.is_comparison(op2)
+