Skip to content

Commit

Permalink
Document and support nested composites
Browse files Browse the repository at this point in the history
Composites can behave in a "nested" fashion by defining the
class in that way.   To make the constructor more convenient,
a callable can be passed to :func:`.composite` instead of the
class itself.  This works now, so add a test to ensure this
pattern remains available.

Change-Id: Ia009f274fca7269f41d6d824e0f70b6fb0ada081
  • Loading branch information
zzzeek committed Dec 12, 2018
1 parent 7206308 commit d4a130b
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 1 deletion.
70 changes: 70 additions & 0 deletions doc/build/orm/composites.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,73 @@ the same expression that the base "greater than" does::
end = composite(Point, x2, y2,
comparator_factory=PointComparator)

Nesting Composites
-------------------

Composite objects can be defined to work in simple nested schemes, by
redefining behaviors within the composite class to work as desired, then
mapping the composite class to the full length of individual columns normally.
Typically, it is convenient to define separate constructors for user-defined
use and generate-from-row use. Below we reorganize the ``Vertex`` class to
itself be a composite object, which is then mapped to a class ``HasVertex``::

from sqlalchemy.orm import composite

class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y

def __composite_values__(self):
return self.x, self.y

def __repr__(self):
return "Point(x=%r, y=%r)" % (self.x, self.y)

def __eq__(self, other):
return isinstance(other, Point) and \
other.x == self.x and \
other.y == self.y

def __ne__(self, other):
return not self.__eq__(other)

class Vertex(object):
def __init__(self, start, end):
self.start = start
self.end = end

@classmethod
def _generate(self, x1, y1, x2, y2):
"""generate a Vertex from a row"""
return Vertex(
Point(x1, y1),
Point(x2, y2)
)

def __composite_values__(self):
return \
self.start.__composite_values__() + \
self.end.__composite_values__()

class HasVertex(Base):
__tablename__ = 'has_vertex'
id = Column(Integer, primary_key=True)
x1 = Column(Integer)
y1 = Column(Integer)
x2 = Column(Integer)
y2 = Column(Integer)

vertex = composite(Vertex._generate, x1, y1, x2, y2)

We can then use the above mapping as::

hv = HasVertex(vertex=Vertex(Point(1, 2), Point(3, 4)))

s.add(hv)
s.commit()

hv = s.query(HasVertex).filter(
HasVertex.vertex == Vertex(Point(1, 2), Point(3, 4))).first()
print(hv.vertex.start)
print(hv.vertex.end)
4 changes: 3 additions & 1 deletion lib/sqlalchemy/orm/descriptor_props.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ def __init__(self, class_, *attrs, **kwargs):
is the :class:`.CompositeProperty`.
:param class\_:
The "composite type" class.
The "composite type" class, or any classmethod or callable which
will produce a new instance of the composite object given the
column values in order.
:param \*cols:
List of Column objects to be mapped.
Expand Down
75 changes: 75 additions & 0 deletions test/orm/test_composites.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from sqlalchemy.testing import fixtures




class PointTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
@classmethod
def define_tables(cls, metadata):
Expand Down Expand Up @@ -365,6 +367,79 @@ def test_default_value(self):
eq_(e.start, None)


class NestedTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
@classmethod
def define_tables(cls, metadata):
Table('stuff', metadata,
Column('id', Integer, primary_key=True,
test_needs_autoincrement=True),
Column("a", String(30)),
Column("b", String(30)),
Column("c", String(30)),
Column("d", String(30)))

def _fixture(self):
class AB(object):
def __init__(self, a, b, cd):
self.a = a
self.b = b
self.cd = cd

@classmethod
def generate(cls, a, b, c, d):
return AB(a, b, CD(c, d))

def __composite_values__(self):
return (self.a, self.b) + self.cd.__composite_values__()

def __eq__(self, other):
return isinstance(other, AB) and \
self.a == other.a and self.b == other.b and \
self.cd == other.cd

def __ne__(self, other):
return not self.__eq__(other)

class CD(object):
def __init__(self, c, d):
self.c = c
self.d = d

def __composite_values__(self):
return (self.c, self.d)

def __eq__(self, other):
return isinstance(other, CD) and \
self.c == other.c and self.d == other.d

def __ne__(self, other):
return not self.__eq__(other)

class Thing(object):
def __init__(self, ab):
self.ab = ab

stuff = self.tables.stuff
mapper(Thing, stuff, properties={
"ab": composite(
AB.generate, stuff.c.a, stuff.c.b, stuff.c.c, stuff.c.d)
})
return Thing, AB, CD

def test_round_trip(self):
Thing, AB, CD = self._fixture()

s = Session()

s.add(Thing(AB('a', 'b', CD('c', 'd'))))
s.commit()

s.close()

t1 = s.query(Thing).filter(
Thing.ab == AB('a', 'b', CD('c', 'd'))).one()
eq_(t1.ab, AB('a', 'b', CD('c', 'd')))

class PrimaryKeyTest(fixtures.MappedTest):
@classmethod
def define_tables(cls, metadata):
Expand Down

0 comments on commit d4a130b

Please sign in to comment.