]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- created a link between QueryContext and SelectionContext; the attribute
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 26 Sep 2007 17:08:19 +0000 (17:08 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 26 Sep 2007 17:08:19 +0000 (17:08 +0000)
dictionary of QueryContext is now passed to SelectionContext inside
of Query.instances(), allowing messages to be passed between the two stages.
- removed the recent "exact match" behavior of Alias objects, they're back to
their usual behavior.
- tightened up the relationship between the Query's generation
  of "eager load" aliases, and Query.instances() which actually grabs the
  eagerly loaded rows.  If the aliases were not specifically generated for
  that statement by EagerLoader, the EagerLoader will not take effect
  when the rows are fetched.  This prevents columns from being grabbed accidentally
  as being part of an eager load when they were not meant for such, which can happen
  with textual SQL as well as some inheritance situations.  It's particularly important
  since the "anonymous aliasing" of columns uses simple integer counts now to generate
  labels.

CHANGES
lib/sqlalchemy/engine/base.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/shard.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/sql/expression.py
test/sql/query.py

diff --git a/CHANGES b/CHANGES
index 5ebec6b471c75dbcd1e6ae17979e84b15d38fa56..0dfde9f625f7550050268b7aa4c6a0ea1696ea5f 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -22,6 +22,16 @@ strong reference to those instances until changes are flushed.
 - Added full list of SQLite reserved keywords so that they get escaped
 properly.
 
+- tightened up the relationship between the Query's generation
+  of "eager load" aliases, and Query.instances() which actually grabs the
+  eagerly loaded rows.  If the aliases were not specifically generated for
+  that statement by EagerLoader, the EagerLoader will not take effect
+  when the rows are fetched.  This prevents columns from being grabbed accidentally 
+  as being part of an eager load when they were not meant for such, which can happen
+  with textual SQL as well as some inheritance situations.  It's particularly important
+  since the "anonymous aliasing" of columns uses simple integer counts now to generate
+  labels.
+  
 - Removed "parameters" argument from clauseelement.compile(), replaced with
   "column_keys".  The parameters sent to execute() only interact with the
   insert/update statement compilation process in terms of the column names
@@ -42,11 +52,6 @@ properly.
 - 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
index 7e9d57d971c5ea5f238e81be5b0cf526c892a77e..ad2b5df96977daa26fc7f8aab67019aae3f687c8 100644 (file)
@@ -1287,19 +1287,9 @@ class ResultProxy(object):
             elif isinstance(key, basestring) and key.lower() in props:
                 rec = props[key.lower()]
             elif isinstance(key, expression.ColumnElement):
-                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
+                label = context.column_labels.get(key._label, key.name).lower()
+                if label in props:
+                    rec = props[label]
             if not "rec" in locals():
                 raise exceptions.NoSuchColumnError("Could not locate column in row for column '%s'" % (str(key)))
 
index e0f7799b7cb27c25e746439e8eca4d74ce17f5af..8da7d917c5658a7865e76fe55d02cdb5c2cc84d2 100644 (file)
@@ -493,10 +493,10 @@ class OperationContext(object):
     Accept ``MapperOption`` objects which may modify its state before proceeding.
     """
 
-    def __init__(self, mapper, options):
+    def __init__(self, mapper, options, attributes=None):
         self.mapper = mapper
         self.options = options
-        self.attributes = {}
+        self.attributes = attributes or {}
         self.recursion_stack = util.Set()
         for opt in util.flatten_iterator(options):
             self.accept_option(opt)
index cb30eafcfe1652734e2700406824ad1c207270b0..0c62737cbefb5b9f49a0b4def9309e149187b973 100644 (file)
@@ -612,16 +612,16 @@ class Query(object):
             raise exceptions.InvalidRequestError('Multiple rows returned for one()')
     
     def __iter__(self):
-        statement = self.compile()
-        statement.use_labels = True
+        context = self._compile_context()
+        context.statement.use_labels = True
         if self._autoflush and not self._populate_existing:
             self.session._autoflush()
-        return self._execute_and_instances(statement)
+        return self._execute_and_instances(context)
     
-    def _execute_and_instances(self, statement):
-        result = self.session.execute(statement, params=self._params, mapper=self.mapper)
+    def _execute_and_instances(self, querycontext):
+        result = self.session.execute(querycontext.statement, params=self._params, mapper=self.mapper)
         try:
-            return iter(self.instances(result))
+            return iter(self.instances(result, querycontext=querycontext))
         finally:
             result.close()
 
@@ -784,10 +784,16 @@ class Query(object):
         
     def compile(self):
         """compiles and returns a SQL statement based on the criterion and conditions within this Query."""
+        return self._compile_context().statement
         
+    def _compile_context(self):
+
+        context = QueryContext(self)
+
         if self._statement:
             self._statement.use_labels = True
-            return self._statement
+            context.statement = self._statement
+            return context
         
         whereclause = self._criterion
 
@@ -807,7 +813,6 @@ class Query(object):
         
         # get/create query context.  get the ultimate compile arguments
         # from there
-        context = QueryContext(self)
         order_by = context.order_by
         from_obj = context.from_obj
         lockmode = context.lockmode
@@ -887,7 +892,7 @@ class Query(object):
                     m = clauses.adapt_clause(m)
                 statement.append_column(m)
                 
-        return statement
+        return context
 
     def _get_entity_clauses(self, m):
         """for tuples added via add_entity() or add_column(), attempt to locate
@@ -1209,17 +1214,27 @@ class SelectionContext(OperationContext):
       Indicates if mappers that have version_id columns should verify
       that instances existing already within the Session should have
       this attribute compared to the freshly loaded value.
+      
+    querycontext
+      the QueryContext, if any, used to generate the executed statement.
+      If present, the attribute dictionary from this Context will be used
+      as the basis for this SelectionContext's attribute dictionary.  This
+      allows query-compile-time operations to send messages to the 
+      result-processing-time operations.
     """
 
     def __init__(self, mapper, session, extension, **kwargs):
         self.populate_existing = kwargs.pop('populate_existing', False)
         self.version_check = kwargs.pop('version_check', False)
+        querycontext = kwargs.pop('querycontext', None)
+        if querycontext:
+            kwargs['attributes'] = querycontext.attributes
         self.session = session
         self.extension = extension
         self.identity_map = {}
         self.stack = LoaderStack()
         super(SelectionContext, self).__init__(mapper, kwargs.pop('with_options', []), **kwargs)
-
+            
     def accept_option(self, opt):
         """Accept a MapperOption which will process (modify) the state
         of this SelectionContext.
index 48e0579666c1fb6757327216e33240823efeb332..c38bcdd96c9fd44c6ce01d7351736aaa4fedbc2d 100644 (file)
@@ -78,19 +78,19 @@ class ShardedQuery(Query):
         q._shard_id = shard_id
         return q
         
-    def _execute_and_instances(self, statement):
+    def _execute_and_instances(self, context):
         if self._shard_id is not None:
-            result = self.session.connection(mapper=self.mapper, shard_id=self._shard_id).execute(statement, **self._params)
+            result = self.session.connection(mapper=self.mapper, shard_id=self._shard_id).execute(context.statement, **self._params)
             try:
-                return iter(self.instances(result))
+                return iter(self.instances(result, querycontext=context))
             finally:
                 result.close()
         else:
             partial = []
             for shard_id in self.query_chooser(self):
-                result = self.session.connection(mapper=self.mapper, shard_id=shard_id).execute(statement, **self._params)
+                result = self.session.connection(mapper=self.mapper, shard_id=shard_id).execute(context.statement, **self._params)
                 try:
-                    partial = partial + list(self.instances(result))
+                    partial = partial + list(self.instances(result, querycontext=context))
                 finally:
                     result.close()
             # if some kind of in memory 'sorting' were done, this is where it would happen
index f3a0029f5797dd4c1ad209dfaf25cd299f978afc..b93993af8dba3640e930c94276ab4394c71c3e32 100644 (file)
@@ -506,12 +506,17 @@ class EagerLoader(AbstractRelationLoader):
                         break
             else:
                 raise exceptions.InvalidRequestError("EagerLoader cannot locate a clause with which to outer join to, in query '%s' %s" % (str(statement), localparent.mapped_table))
-            
+        
+        # create AliasedClauses object to build up the eager query.  this is cached after 1st creation.    
         try:
             clauses = self.clauses[path]
         except KeyError:
             clauses = mapperutil.PropertyAliasedClauses(self.parent_property, self.parent_property.polymorphic_primaryjoin, self.parent_property.polymorphic_secondaryjoin, parentclauses)
             self.clauses[path] = clauses
+
+        # place the "row_decorator" from the AliasedClauses into the QueryContext, where it will
+        # be picked up in create_row_processor() when results are fetched
+        context.attributes[("eager_row_processor", path)] = clauses.row_decorator
         
         if self.secondaryjoin is not None:
             statement._outerjoin = sql.outerjoin(towrap, clauses.secondary, clauses.primaryjoin).outerjoin(clauses.alias, clauses.secondaryjoin)
@@ -545,19 +550,18 @@ class EagerLoader(AbstractRelationLoader):
             # custom row decoration function, placed in the selectcontext by the 
             # contains_eager() mapper option
             decorator = selectcontext.attributes[("eager_row_processor", self.parent_property)]
+            # key was present, but no decorator; therefore just use the row as is
             if decorator is None:
                 decorator = lambda row: row
+        # check for an AliasedClauses row decorator that was set up by query._compile_context().
+        # a further refactoring (described in [ticket:777]) will simplify this so that the
+        # contains_eager() option generates the same key as this one
+        elif ("eager_row_processor", path) in selectcontext.attributes:
+            decorator = selectcontext.attributes[("eager_row_processor", path)]
         else:
-            try:
-                # decorate the row according to the stored AliasedClauses for this eager load
-                clauses = self.clauses[path]
-                decorator = clauses.row_decorator
-            except KeyError, k:
-                # no stored AliasedClauses: eager loading was not set up in the query and
-                # AliasedClauses never got initialized
-                if self._should_log_debug:
-                    self.logger.debug("Could not locate aliased clauses for key: " + str(path))
-                return None
+            if self._should_log_debug:
+                self.logger.debug("Could not locate aliased clauses for key: " + str(path))
+            return None
 
         try:
             decorated_row = decorator(row)
index d649fc0ff08e2bd633c824577b9761b68e2f87d4..4bacf4be3f51b48d5ba66cdefd5d91a10e7f8e20 100644 (file)
@@ -2425,14 +2425,6 @@ class Alias(FromClause):
     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):
index e64425e914eb825d6f438d99fe08fef494c1979b..a519dd974bc76084ed163a3e2dc07e5c2e577778 100644 (file)
@@ -429,22 +429,6 @@ class QueryTest(PersistTest):
         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)