Commits

Mike Bayer  committed 42584d9

- columns from Alias objects, when used to target result-row columns, must match exactly
to the label used in the generated statement. This is so searching for columns in a
result row which match aliases won't accidentally match non-aliased columns.
fixes errors which can arise in eager loading scenarios.

  • Participants
  • Parent commits 2c1787a

Comments (0)

Files changed (6)

 
 - added "schema" argument to Sequence; use this with Postgres /Oracle when the sequence is
   located in an alternate schema.  Implements part of [ticket:584], should fix [ticket:761].
-  
+
+- columns from Alias objects, when used to target result-row columns, must match exactly
+  to the label used in the generated statement.  This is so searching for columns in a 
+  result row which match aliases won't accidentally match non-aliased columns.
+  fixes errors which can arise in eager loading scenarios.
+    
 - Fixed reflection of the empty string for mysql enums.
 
 - Added 'passive_deletes="all"' flag to relation(), disables all nulling-out

File lib/sqlalchemy/engine/base.py

             elif isinstance(key, basestring) and key.lower() in props:
                 rec = props[key.lower()]
             elif isinstance(key, expression.ColumnElement):
-                label = context.column_labels.get(key._label, key.name).lower()
-                if label in props:
-                    rec = props[label]
-
+                try:
+                    if getattr(key, '_exact_match', False):
+                        # exact match flag means the label must be present in the
+                        # generated column_labels
+                        label = context.column_labels[key._label].lower()
+                    else:
+                        # otherwise, fall back to the straight name of the column
+                        # if not in generated labels
+                        label = context.column_labels.get(key._label, key.name).lower()
+                    if label in props:
+                        rec = props[label]
+                except KeyError:
+                    pass
             if not "rec" in locals():
                 raise exceptions.NoSuchColumnError("Could not locate column in row for column '%s'" % (str(key)))
 

File lib/sqlalchemy/orm/strategies.py

                 selectcontext.stack.pop()
 
             selectcontext.stack.pop()
+
+            if self._should_log_debug:
+                self.logger.debug("Returning eager instance loader for %s" % str(self))
+
             return (execute, execute, None)
         else:
             if self._should_log_debug:

File lib/sqlalchemy/sql/expression.py

     def _get_from_objects(self, **modifiers):
         return [self]
 
+    def _proxy_column(self, column):
+        c = column._make_proxy(self)
+        # send a note to ResultProxy to not "approximate"
+        # this column based on its name when targeting result columns
+        # see test/sql/query.py QueryTest.test_exact_match
+        c._exact_match = True
+        return c
+
     bind = property(lambda s: s.selectable.bind)
 
 class _ColumnElementAdapter(ColumnElement):

File test/orm/eager_relations.py

 
             ] == q.all()
         self.assert_sql_count(testbase.db, go, 1)
+
+    def test_no_false_hits(self):
+        """test that eager loaders don't interpret main table columns as part of their eager load."""
+        
+        mapper(User, users, properties={
+            'addresses':relation(Address, lazy=False),
+            'orders':relation(Order, lazy=False)
+        })
+        mapper(Address, addresses)
+        mapper(Order, orders)
+        
+        allusers = create_session().query(User).all()
+        
+        # using a textual select, the columns will be 'id' and 'name'.
+        # the eager loaders have aliases which should not hit on those columns, they should 
+        # be required to locate only their aliased/fully table qualified column name.
+        noeagers = create_session().query(User).from_statement("select * from users").all()
+        assert 'orders' not in noeagers[0].__dict__
+        assert 'addresses' not in noeagers[0].__dict__
         
     def test_limit(self):
         """test limit operations combined with lazy-load relationships."""

File test/sql/query.py

         self.assertEqual([x.lower() for x in r.keys()], ['user_name', 'user_id'])
         self.assertEqual(r.values(), ['foo', 1])
     
+    def test_exact_match(self):
+        """test that an Alias object only targets result columns that were generated
+        from that Alias.   This is also part of eager_relations.py/test_no_false_hits.
+        """
+        
+        users.insert().execute(user_id=1, user_name='ed')
+        users_alias = users.alias()
+        result = users.select().execute()
+        row = result.fetchone()
+        assert users_alias.c.user_id not in row
+        
+        result = users_alias.select().execute()
+        row = result.fetchone()
+        assert users_alias.c.user_id in row
+        
+        
     @testing.unsupported('oracle', 'firebird') 
     def test_column_accessor_shadow(self):
         meta = MetaData(testbase.db)