summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-10-03 17:06:55 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-10-03 17:06:55 -0400
commita83378b64005971fe97dff270641bce4967dbb53 (patch)
treebbfe8fbf510ddde2c71ff4e26cfe893ca9abce92 /test
parent78c5249bf7d39c3aaa4954c7815a1ef48f2776db (diff)
downloadsqlalchemy-a83378b64005971fe97dff270641bce4967dbb53.tar.gz
- A new construct :class:`.Bundle` is added, which allows for specification
of groups of column expressions to a :class:`.Query` construct. The group of columns are returned as a single tuple by default. The behavior of :class:`.Bundle` can be overridden however to provide any sort of result processing to the returned row. One example included is :attr:`.Composite.Comparator.bundle`, which applies a bundled form of a "composite" mapped attribute. [ticket:2824] - The :func:`.composite` construct now maintains the return object when used in a column-oriented :class:`.Query`, rather than expanding out into individual columns. This makes use of the new :class:`.Bundle` feature internally. This behavior is backwards incompatible; to select from a composite column which will expand out, use ``MyClass.some_composite.clauses``.
Diffstat (limited to 'test')
-rw-r--r--test/orm/test_bundle.py245
-rw-r--r--test/orm/test_composites.py49
2 files changed, 291 insertions, 3 deletions
diff --git a/test/orm/test_bundle.py b/test/orm/test_bundle.py
new file mode 100644
index 000000000..305f8d3c6
--- /dev/null
+++ b/test/orm/test_bundle.py
@@ -0,0 +1,245 @@
+from sqlalchemy.testing import fixtures, eq_
+from sqlalchemy.testing.schema import Table, Column
+from sqlalchemy.orm import Bundle, Session
+from sqlalchemy.testing import AssertsCompiledSQL
+from sqlalchemy import Integer, select, ForeignKey, String, func
+from sqlalchemy.orm import mapper, relationship, aliased
+
+class BundleTest(fixtures.MappedTest, AssertsCompiledSQL):
+ __dialect__ = 'default'
+
+ run_inserts = 'once'
+ run_setup_mappers = 'once'
+ run_deletes = None
+
+ @classmethod
+ def define_tables(cls, metadata):
+ Table('data', metadata,
+ Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True),
+ Column('d1', String(10)),
+ Column('d2', String(10)),
+ Column('d3', String(10))
+ )
+
+ Table('other', metadata,
+ Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+ Column('data_id', ForeignKey('data.id')),
+ Column('o1', String(10))
+ )
+
+ @classmethod
+ def setup_classes(cls):
+ class Data(cls.Basic):
+ pass
+ class Other(cls.Basic):
+ pass
+
+ @classmethod
+ def setup_mappers(cls):
+ mapper(cls.classes.Data, cls.tables.data, properties={
+ 'others': relationship(cls.classes.Other)
+ })
+ mapper(cls.classes.Other, cls.tables.other)
+
+ @classmethod
+ def insert_data(cls):
+ sess = Session()
+ sess.add_all([
+ cls.classes.Data(d1='d%dd1' % i, d2='d%dd2' % i, d3='d%dd3' % i,
+ others=[cls.classes.Other(o1="d%do%d" % (i, j)) for j in range(5)])
+ for i in range(10)
+ ])
+ sess.commit()
+
+ def test_c_attr(self):
+ Data = self.classes.Data
+
+ b1 = Bundle('b1', Data.d1, Data.d2)
+
+ self.assert_compile(
+ select([b1.c.d1, b1.c.d2]),
+ "SELECT data.d1, data.d2 FROM data"
+ )
+
+ def test_result(self):
+ Data = self.classes.Data
+ sess = Session()
+
+ b1 = Bundle('b1', Data.d1, Data.d2)
+
+ eq_(
+ sess.query(b1).filter(b1.c.d1.between('d3d1', 'd5d1')).all(),
+ [(('d3d1', 'd3d2'),), (('d4d1', 'd4d2'),), (('d5d1', 'd5d2'),)]
+ )
+
+ def test_subclass(self):
+ Data = self.classes.Data
+ sess = Session()
+
+ class MyBundle(Bundle):
+ def create_row_processor(self, query, procs, labels):
+ def proc(row, result):
+ return dict(
+ zip(labels, (proc(row, result) for proc in procs))
+ )
+ return proc
+
+ b1 = MyBundle('b1', Data.d1, Data.d2)
+
+ eq_(
+ sess.query(b1).filter(b1.c.d1.between('d3d1', 'd5d1')).all(),
+ [({'d2': 'd3d2', 'd1': 'd3d1'},),
+ ({'d2': 'd4d2', 'd1': 'd4d1'},),
+ ({'d2': 'd5d2', 'd1': 'd5d1'},)]
+ )
+
+ def test_multi_bundle(self):
+ Data = self.classes.Data
+ Other = self.classes.Other
+
+ d1 = aliased(Data)
+
+ b1 = Bundle('b1', d1.d1, d1.d2)
+ b2 = Bundle('b2', Data.d1, Other.o1)
+
+ sess = Session()
+
+ q = sess.query(b1, b2).join(Data.others).join(d1, d1.id == Data.id).\
+ filter(b1.c.d1 == 'd3d1')
+ eq_(
+ q.all(),
+ [
+ (('d3d1', 'd3d2'), ('d3d1', 'd3o0')),
+ (('d3d1', 'd3d2'), ('d3d1', 'd3o1')),
+ (('d3d1', 'd3d2'), ('d3d1', 'd3o2')),
+ (('d3d1', 'd3d2'), ('d3d1', 'd3o3')),
+ (('d3d1', 'd3d2'), ('d3d1', 'd3o4'))]
+ )
+
+ def test_bundle_nesting(self):
+ Data = self.classes.Data
+ sess = Session()
+
+ b1 = Bundle('b1', Data.d1, Bundle('b2', Data.d2, Data.d3))
+
+ eq_(
+ sess.query(b1).
+ filter(b1.c.d1.between('d3d1', 'd7d1')).
+ filter(b1.c.b2.c.d2.between('d4d2', 'd6d2')).
+ all(),
+ [(('d4d1', ('d4d2', 'd4d3')),), (('d5d1', ('d5d2', 'd5d3')),),
+ (('d6d1', ('d6d2', 'd6d3')),)]
+ )
+
+ def test_bundle_nesting_unions(self):
+ Data = self.classes.Data
+ sess = Session()
+
+ b1 = Bundle('b1', Data.d1, Bundle('b2', Data.d2, Data.d3))
+
+ q1 = sess.query(b1).\
+ filter(b1.c.d1.between('d3d1', 'd7d1')).\
+ filter(b1.c.b2.c.d2.between('d4d2', 'd5d2'))
+
+ q2 = sess.query(b1).\
+ filter(b1.c.d1.between('d3d1', 'd7d1')).\
+ filter(b1.c.b2.c.d2.between('d5d2', 'd6d2'))
+
+ eq_(
+ q1.union(q2).all(),
+ [(('d4d1', ('d4d2', 'd4d3')),), (('d5d1', ('d5d2', 'd5d3')),),
+ (('d6d1', ('d6d2', 'd6d3')),)]
+ )
+
+ # naming structure is preserved
+ row = q1.union(q2).first()
+ eq_(row.b1.d1, 'd4d1')
+ eq_(row.b1.b2.d2, 'd4d2')
+
+
+ def test_query_count(self):
+ Data = self.classes.Data
+ b1 = Bundle('b1', Data.d1, Data.d2)
+ eq_(Session().query(b1).count(), 10)
+
+ def test_join_relationship(self):
+ Data = self.classes.Data
+
+ sess = Session()
+ b1 = Bundle('b1', Data.d1, Data.d2)
+ q = sess.query(b1).join(Data.others)
+ self.assert_compile(q,
+ "SELECT data.d1 AS data_d1, data.d2 AS data_d2 FROM data "
+ "JOIN other ON data.id = other.data_id"
+ )
+
+ def test_join_selectable(self):
+ Data = self.classes.Data
+ Other = self.classes.Other
+
+ sess = Session()
+ b1 = Bundle('b1', Data.d1, Data.d2)
+ q = sess.query(b1).join(Other)
+ self.assert_compile(q,
+ "SELECT data.d1 AS data_d1, data.d2 AS data_d2 FROM data "
+ "JOIN other ON data.id = other.data_id"
+ )
+
+
+ def test_joins_from_adapted_entities(self):
+ Data = self.classes.Data
+
+ # test for #1853 in terms of bundles
+ # specifically this exercises adapt_to_selectable()
+
+ b1 = Bundle('b1', Data.id, Data.d1, Data.d2)
+
+ session = Session()
+ first = session.query(b1)
+ second = session.query(b1)
+ unioned = first.union(second)
+ subquery = session.query(Data.id).subquery()
+ joined = unioned.outerjoin(subquery, subquery.c.id == Data.id)
+ joined = joined.order_by(Data.id, Data.d1, Data.d2)
+
+ self.assert_compile(
+ joined,
+ "SELECT anon_1.data_id AS anon_1_data_id, anon_1.data_d1 AS anon_1_data_d1, "
+ "anon_1.data_d2 AS anon_1_data_d2 FROM "
+ "(SELECT data.id AS data_id, data.d1 AS data_d1, data.d2 AS data_d2 FROM "
+ "data UNION SELECT data.id AS data_id, data.d1 AS data_d1, "
+ "data.d2 AS data_d2 FROM data) AS anon_1 "
+ "LEFT OUTER JOIN (SELECT data.id AS id FROM data) AS anon_2 "
+ "ON anon_2.id = anon_1.data_id "
+ "ORDER BY anon_1.data_id, anon_1.data_d1, anon_1.data_d2")
+
+ # tuple nesting still occurs
+ eq_(
+ joined.all(),
+ [((1, 'd0d1', 'd0d2'),), ((2, 'd1d1', 'd1d2'),),
+ ((3, 'd2d1', 'd2d2'),), ((4, 'd3d1', 'd3d2'),),
+ ((5, 'd4d1', 'd4d2'),), ((6, 'd5d1', 'd5d2'),),
+ ((7, 'd6d1', 'd6d2'),), ((8, 'd7d1', 'd7d2'),),
+ ((9, 'd8d1', 'd8d2'),), ((10, 'd9d1', 'd9d2'),)]
+ )
+
+ def test_clause_expansion(self):
+ Data = self.classes.Data
+
+ b1 = Bundle('b1', Data.id, Data.d1, Data.d2)
+
+ sess = Session()
+ self.assert_compile(
+ sess.query(Data).order_by(b1),
+ "SELECT data.id AS data_id, data.d1 AS data_d1, "
+ "data.d2 AS data_d2, data.d3 AS data_d3 FROM data "
+ "ORDER BY data.id, data.d1, data.d2"
+ )
+
+ self.assert_compile(
+ sess.query(func.row_number().over(order_by=b1)),
+ "SELECT row_number() OVER (ORDER BY data.id, data.d1, data.d2) "
+ "AS anon_1 FROM data"
+ )
+
diff --git a/test/orm/test_composites.py b/test/orm/test_composites.py
index 5e7b91f3e..eabc9ca7b 100644
--- a/test/orm/test_composites.py
+++ b/test/orm/test_composites.py
@@ -214,17 +214,45 @@ class PointTest(fixtures.MappedTest):
((), [Point(x=None, y=None)], ())
)
- def test_query_cols(self):
+ def test_query_cols_legacy(self):
Edge = self.classes.Edge
sess = self._fixture()
eq_(
- sess.query(Edge.start, Edge.end).all(),
+ sess.query(Edge.start.clauses, Edge.end.clauses).all(),
[(3, 4, 5, 6), (14, 5, 2, 7)]
)
+ def test_query_cols(self):
+ Edge = self.classes.Edge
+ Point = self.classes.Point
+
+ sess = self._fixture()
+
+ start, end = Edge.start, Edge.end
+
+ eq_(
+ sess.query(start, end).filter(start == Point(3, 4)).all(),
+ [(Point(3, 4), Point(5, 6))]
+ )
+
+ def test_query_cols_labeled(self):
+ Edge = self.classes.Edge
+ Point = self.classes.Point
+
+ sess = self._fixture()
+
+ start, end = Edge.start, Edge.end
+
+ row = sess.query(start.label('s1'), end).filter(start == Point(3, 4)).first()
+ eq_(row.s1.x, 3)
+ eq_(row.s1.y, 4)
+ eq_(row.end.x, 5)
+ eq_(row.end.y, 6)
+
def test_delete(self):
+ Point = self.classes.Point
Graph, Edge = self.classes.Graph, self.classes.Edge
sess = self._fixture()
@@ -235,7 +263,10 @@ class PointTest(fixtures.MappedTest):
sess.flush()
eq_(
sess.query(Edge.start, Edge.end).all(),
- [(3, 4, 5, 6), (14, 5, None, None)]
+ [
+ (Point(x=3, y=4), Point(x=5, y=6)),
+ (Point(x=14, y=5), Point(x=None, y=None))
+ ]
)
def test_save_null(self):
@@ -863,3 +894,15 @@ class ComparatorTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
"edge_1.x2, edge_1.y2"
)
+ def test_clause_expansion(self):
+ self._fixture(False)
+ Edge = self.classes.Edge
+ from sqlalchemy.orm import configure_mappers
+ configure_mappers()
+
+ self.assert_compile(
+ select([Edge]).order_by(Edge.start),
+ "SELECT edge.id, edge.x1, edge.y1, edge.x2, edge.y2 FROM edge "
+ "ORDER BY edge.x1, edge.y1"
+ )
+