]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- reworked the way the "select_wraps_for" expression is
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 21 Feb 2016 01:22:38 +0000 (20:22 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 21 Feb 2016 01:22:38 +0000 (20:22 -0500)
handled within visit_select(); this attribute was added in the
1.0 series to accommodate the subquery wrapping behavior of
SQL Server and Oracle while also working with positional
column targeting and no longer relying upon "key fallback"
in order to target columns in such a statement.  The IBM DB2
third-party dialect also has this use case, but its implementation
is using regular expressions to rewrite the textual SELECT only
and does not make use of a "wrapped" select at this time.
The logic no longer attempts to reconcile proxy set collections as
this was not deterministic, and instead assumes that the select()
and the wrapper select() match their columns postionally,
at least for the column positions they have in common,
so it is now very simple and safe.  fixes #3657.
- as a side effect of #3657 it was also revealed that the
strategy of calling upon a ResultProxy._getter was not
correctly calling into NoSuchColumnError when an expected
column was not present, and instead returned None up to
loading.instances() to produce NoneType failures; added
a raiseerr argument to _getter() which is called when we
aren't expecting None, fixes #3658.

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/engine/result.py
lib/sqlalchemy/orm/loading.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/sql/compiler.py
test/dialect/mssql/test_compiler.py
test/orm/test_loading.py
test/sql/test_compiler.py
test/sql/test_resultset.py

index 1e10b97d04a8b6010396ca6977e1b58feebcede3..fa1be4c16e94ede600b51f3e9b9a8a87e0a5c974 100644 (file)
     .. include:: changelog_07.rst
         :start-line: 5
 
+.. changelog::
+    :version: 1.0.13
+
+    .. change::
+        :tags: bug, orm
+        :tickets: 3658
+
+        Fixed regression appearing in the 1.0 series in ORM loading where the
+        exception raised for an expected column missing would incorrectly
+        be a ``NoneType`` error, rather than the expected
+        :class:`.NoSuchColumnError`.
+
+    .. change::
+        :tags: bug, mssql, oracle
+        :tickets: 3657
+
+        Fixed regression appearing in the 1.0 series which would cause the Oracle
+        and SQL Server dialects to incorrectly account for result set columns
+        when these dialects would wrap a SELECT in a subquery in order to
+        provide LIMIT/OFFSET behavior, and the original SELECT statement
+        referred to the same column multiple times, such as a column and
+        a label of that same column.  This issue is related
+        to :ticket:`3658` in that when the error occurred, it would also
+        cause a ``NoneType`` error, rather than reporting that it couldn't
+        locate a column.
+
 .. changelog::
     :version: 1.0.12
     :released: February 15, 2016
index c069fcedf21f163d26120ddf6bf45808ac32417f..f09b0b40b36430194bdd8c3ebf157a18217ca3f1 100644 (file)
@@ -561,11 +561,11 @@ class ResultMetaData(object):
         else:
             return self._key_fallback(key, False) is not None
 
-    def _getter(self, key):
+    def _getter(self, key, raiseerr=True):
         if key in self._keymap:
             processor, obj, index = self._keymap[key]
         else:
-            ret = self._key_fallback(key, False)
+            ret = self._key_fallback(key, raiseerr)
             if ret is None:
                 return None
             processor, obj, index = ret
@@ -640,13 +640,13 @@ class ResultProxy(object):
             context.engine._should_log_debug()
         self._init_metadata()
 
-    def _getter(self, key):
+    def _getter(self, key, raiseerr=True):
         try:
             getter = self._metadata._getter
         except AttributeError:
             return self._non_result(None)
         else:
-            return getter(key)
+            return getter(key, raiseerr)
 
     def _has_key(self, key):
         try:
index b8bc205d48a4e29a4af8c72b8dd12d5a634f61d8..d457f3c6375a254d004b6a76d7890e12dd9fb3fd 100644 (file)
@@ -316,7 +316,7 @@ def _instance_processor(
             else:
                 if adapter:
                     col = adapter.columns[col]
-                getter = result._getter(col)
+                getter = result._getter(col, False)
                 if getter:
                     populators["quick"].append((prop.key, getter))
                 else:
index 7d816e6261958000c01d470ff56a154be9feeea4..370cb974b52861c9dae660f44b675f000bd538fe 100644 (file)
@@ -174,7 +174,7 @@ class ColumnLoader(LoaderStrategy):
         for col in self.columns:
             if adapter:
                 col = adapter.columns[col]
-            getter = result._getter(col)
+            getter = result._getter(col, False)
             if getter:
                 populators["quick"].append((self.key, getter))
                 break
index a2fc0fe68a77d1eb0b7f9e909c188114356f8f35..b75dc1c076d39137a530045770fa949d5b9b9bf8 100644 (file)
@@ -1638,15 +1638,11 @@ class SQLCompiler(Compiled):
         if populate_result_map and select_wraps_for is not None:
             # if this select is a compiler-generated wrapper,
             # rewrite the targeted columns in the result map
-            wrapped_inner_columns = set(select_wraps_for.inner_columns)
+
             translate = dict(
-                (outer, inner.pop()) for outer, inner in [
-                    (
-                        outer,
-                        outer.proxy_set.intersection(wrapped_inner_columns))
-                    for outer in select.inner_columns
-                ] if inner
+                zip(select.inner_columns, select_wraps_for.inner_columns)
             )
+
             self._result_columns = [
                 (key, name, tuple(translate.get(o, o) for o in obj), type_)
                 for key, name, obj, type_ in self._result_columns
index d91c79db25f1f8d3f5935c5159a4061698751bb3..b59ca4fd10d71ea7582a07824f7044e7f117efa6 100644 (file)
@@ -1,5 +1,5 @@
 # -*- encoding: utf-8
-from sqlalchemy.testing import eq_
+from sqlalchemy.testing import eq_, is_
 from sqlalchemy import schema
 from sqlalchemy.sql import table, column
 from sqlalchemy.databases import mssql
@@ -521,6 +521,30 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
         assert t.c.x in set(c._create_result_map()['x'][1])
         assert t.c.y in set(c._create_result_map()['y'][1])
 
+    def test_limit_offset_w_ambiguous_cols(self):
+        t = table('t', column('x', Integer), column('y', Integer))
+
+        cols = [t.c.x, t.c.x.label('q'), t.c.x.label('p'), t.c.y]
+        s = select(cols).where(t.c.x == 5).order_by(t.c.y).limit(10).offset(20)
+
+        self.assert_compile(
+            s,
+            "SELECT anon_1.x, anon_1.q, anon_1.p, anon_1.y "
+            "FROM (SELECT t.x AS x, t.x AS q, t.x AS p, t.y AS y, "
+            "ROW_NUMBER() OVER (ORDER BY t.y) AS mssql_rn "
+            "FROM t "
+            "WHERE t.x = :x_1) AS anon_1 "
+            "WHERE mssql_rn > :param_1 AND mssql_rn <= :param_2 + :param_1",
+            checkparams={'param_1': 20, 'param_2': 10, 'x_1': 5}
+        )
+        c = s.compile(dialect=mssql.MSDialect())
+        eq_(len(c._result_columns), 4)
+
+        result_map = c._create_result_map()
+
+        for col in cols:
+            is_(result_map[col.key][1][0], col)
+
     def test_limit_offset_with_correlated_order_by(self):
         t1 = table('t1', column('x', Integer), column('y', Integer))
         t2 = table('t2', column('x', Integer), column('y', Integer))
index f86477ec293d28a057a459472a0ef9ff7787b097..6f3f6a016fb0ed0a51cfb0655091bc743fa2197f 100644 (file)
@@ -1,8 +1,11 @@
 from . import _fixtures
 from sqlalchemy.orm import loading, Session, aliased
-from sqlalchemy.testing.assertions import eq_, assert_raises
+from sqlalchemy.testing.assertions import eq_, \
+    assert_raises, assert_raises_message
 from sqlalchemy.util import KeyedTuple
 from sqlalchemy.testing import mock
+from sqlalchemy import select
+from sqlalchemy import exc
 # class GetFromIdentityTest(_fixtures.FixtureTest):
 # class LoadOnIdentTest(_fixtures.FixtureTest):
 # class InstanceProcessorTest(_fixture.FixtureTest):
@@ -34,6 +37,19 @@ class InstancesTest(_fixtures.FixtureTest):
         )
         assert cursor.close.called, "Cursor wasn't closed"
 
+    def test_row_proc_not_created(self):
+        User = self.classes.User
+        s = Session()
+
+        q = s.query(User.id, User.name)
+        stmt = select([User.id])
+
+        assert_raises_message(
+            exc.NoSuchColumnError,
+            "Could not locate column in row for column 'users.name'",
+            q.from_statement(stmt).all
+        )
+
 
 class MergeResultTest(_fixtures.FixtureTest):
     run_setup_mappers = 'once'
index f11a4e61afd7471dbea904cfcad4cbe9412d9e2e..1c5dc234025f5103d2fc5f9c269c8bcefd8d2769 100644 (file)
@@ -3855,3 +3855,26 @@ class ResultMapTest(fixtures.TestBase):
                     (table1.c.description, 'description', 'description'),
                     table1.c.description.type)}
         )
+
+    def test_select_wraps_for_translate_ambiguity(self):
+        # test for issue #3657
+        t = table('a', column('x'), column('y'), column('z'))
+
+        l1, l2, l3 = t.c.z.label('a'), t.c.x.label('b'), t.c.x.label('c')
+        orig = [t.c.x, t.c.y, l1, l2, l3]
+        stmt = select(orig)
+        wrapped = stmt._generate()
+        wrapped = wrapped.column(
+            func.ROW_NUMBER().over(order_by=t.c.z)).alias()
+
+        wrapped_again = select([c for c in wrapped.c])
+
+        compiled = wrapped_again.compile(
+            compile_kwargs={'select_wraps_for': stmt})
+
+        proxied = [obj[0] for (k, n, obj, type_) in compiled._result_columns]
+        for orig_obj, proxied_obj in zip(
+            orig,
+            proxied
+        ):
+            is_(orig_obj, proxied_obj)
index bd2b8c0aeb40917b4f5f0c2f603e587eed9ad286..aaeb82fa4c8a143e00980d0b5dbcd2b42ade84cf 100644 (file)
@@ -204,7 +204,8 @@ class ResultProxyTest(fixtures.TablesTest):
                     lambda: result[0][addresses.c.address_id])
 
     def test_column_error_printing(self):
-        row = testing.db.execute(select([1])).first()
+        result = testing.db.execute(select([1]))
+        row = result.first()
 
         class unprintable(object):
 
@@ -219,6 +220,14 @@ class ResultProxyTest(fixtures.TablesTest):
             (Column("q", Integer) + 12, r"q \+ :q_1"),
             (unprintable(), "unprintable element.*"),
         ]:
+            assert_raises_message(
+                exc.NoSuchColumnError,
+                msg % repl,
+                result._getter, accessor
+            )
+
+            is_(result._getter(accessor, False), None)
+
             assert_raises_message(
                 exc.NoSuchColumnError,
                 msg % repl,