]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed bug regarding calculation of "from" list
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 5 Sep 2011 23:12:12 +0000 (19:12 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 5 Sep 2011 23:12:12 +0000 (19:12 -0400)
for a select() element.  The "from" calc is now
delayed, so that if the construct uses a Column
object that is not yet attached to a Table,
but is later associated with a Table, it generates
SQL using the table as a FROM.   This change
impacted fairly deeply the mechanics of how
the FROM list as well as the "correlates" collection
is calculated, as some "clause adaption" schemes
(these are used very heavily in the ORM)
were relying upon the fact that the "froms"
collection would typically be cached before the
adaption completed.   The rework allows it
such that the "froms" collection can be cleared
and re-generated at any time.  [ticket:2261]
- RelationshipProperty.Comparator._criterion_exists()
adds an "_orm_adapt" annotation to the correlates target,
to work with the change in [ticket:2261].   It's not clear
if the change to correlation+adaption mechanics will affect end user
code yet.
- FromClause now uses group_expirable_memoized_property for
late-generated values like primary key, _columns, etc.
The Select class adds some tokens to this object and has the
nice effect that FromClause doesn't need to know about
Select's names anymore.   An additional change might be to
have Select use a different group_expirable_memoized_property
so that it's collection of attribute names are specific to
Select though this isn't really necessary right now.

CHANGES
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/sql/expression.py
lib/sqlalchemy/util/langhelpers.py
test/orm/inheritance/test_query.py
test/orm/test_selectable.py
test/sql/test_selectable.py

diff --git a/CHANGES b/CHANGES
index 563e36df8b19e0b4cd78b1b57157a6c9f9c0618f..62267025d9818419c575018cd8d241f1afb8078a 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -34,6 +34,23 @@ CHANGES
     on a new mapper would establish a backref on the
     first mapper.
 
+-sql
+  - Fixed bug regarding calculation of "from" list 
+    for a select() element.  The "from" calc is now
+    delayed, so that if the construct uses a Column
+    object that is not yet attached to a Table,
+    but is later associated with a Table, it generates
+    SQL using the table as a FROM.   This change
+    impacted fairly deeply the mechanics of how 
+    the FROM list as well as the "correlates" collection
+    is calculated, as some "clause adaption" schemes
+    (these are used very heavily in the ORM)
+    were relying upon the fact that the "froms" 
+    collection would typically be cached before the 
+    adaption completed.   The rework allows it
+    such that the "froms" collection can be cleared
+    and re-generated at any time.  [ticket:2261]
+
 - schema
   - Added a slightly nicer __repr__() to SchemaItem
     classes.  Note the repr here can't fully support
index de532794cf371432830aecd507a0307a8b0f8f86..1b1eef6d25e8639436b4ebeb16b539a1019db924 100644 (file)
@@ -449,7 +449,8 @@ class RelationshipProperty(StrategizedProperty):
 
             crit = j & criterion
 
-            return sql.exists([1], crit, from_obj=dest).correlate(source)
+            return sql.exists([1], crit, from_obj=dest).\
+                            correlate(source._annotate({'_orm_adapt':True}))
 
         def any(self, criterion=None, **kwargs):
             """Produce an expression that tests a collection against
index 151a321f59a3abdf2c7179e6134769c2666e90a5..0823137248037f890e10d6e036167d51690ec649 100644 (file)
@@ -59,9 +59,9 @@ def nullsfirst(column):
     e.g.::
 
       someselect.order_by(desc(table1.mycol).nullsfirst())
-      
+
     produces::
-    
+
       ORDER BY mycol DESC NULLS FIRST
 
     """
@@ -73,9 +73,9 @@ def nullslast(column):
     e.g.::
 
       someselect.order_by(desc(table1.mycol).nullslast())
-      
+
     produces::
-    
+
         ORDER BY mycol DESC NULLS LAST
 
     """
@@ -87,9 +87,9 @@ def desc(column):
     e.g.::
 
       someselect.order_by(desc(table1.mycol))
-      
+
     produces::
-    
+
         ORDER BY mycol DESC
 
     """
@@ -101,9 +101,9 @@ def asc(column):
     e.g.::
 
       someselect.order_by(asc(table1.mycol))
-      
+
     produces::
-    
+
       ORDER BY mycol ASC
 
     """
@@ -171,9 +171,9 @@ def select(columns=None, whereclause=None, from_obj=[], **kwargs):
     either :func:`text()` or :func:`literal_column()` constructs.
 
     See also:
-    
+
     :ref:`coretutorial_selecting` - Core Tutorial description of :func:`.select`.
-    
+
     :param columns:
       A list of :class:`.ClauseElement` objects, typically
       :class:`.ColumnElement` objects or subclasses, which will form the
@@ -227,15 +227,15 @@ def select(columns=None, whereclause=None, from_obj=[], **kwargs):
     :param distinct=False:
       when ``True``, applies a ``DISTINCT`` qualifier to the columns
       clause of the resulting statement.
-      
+
       The boolean argument may also be a column expression or list
       of column expressions - this is a special calling form which
       is understood by the Postgresql dialect to render the
       ``DISTINCT ON (<columns>)`` syntax.
-      
+
       ``distinct`` is also available via the :meth:`~.Select.distinct`
       generative method.
-      
+
       .. note:: The ``distinct`` keyword's acceptance of a string
         argument for usage with MySQL is deprecated.  Use
         the ``prefixes`` argument or :meth:`~.Select.prefix_with`.
@@ -287,10 +287,10 @@ def select(columns=None, whereclause=None, from_obj=[], **kwargs):
       The format of the label is <tablename>_<column>.  The "c"
       collection of the resulting :class:`.Select` object will use these
       names as well for targeting column members.
-      
+
       use_labels is also available via the :meth:`~._SelectBase.apply_labels`
       generative method.
-      
+
     """
     return Select(columns, whereclause=whereclause, from_obj=from_obj,
                   **kwargs)
@@ -317,7 +317,7 @@ def insert(table, values=None, inline=False, **kwargs):
     :class:`~.schema.Table`.
 
     See also:
-    
+
     :ref:`coretutorial_insert_expressions` - Core Tutorial description of the :func:`.insert` construct.
 
     :param table: The table to be inserted into.
@@ -456,15 +456,15 @@ def not_(clause):
 
 def distinct(expr):
     """Return a ``DISTINCT`` clause.
-    
+
     e.g.::
-    
+
         distinct(a)
-        
+
     renders::
-    
+
         DISTINCT a
-        
+
     """
     expr = _literal_as_binds(expr)
     return _UnaryExpression(expr, operator=operators.distinct_op, type_=expr.type)
@@ -560,13 +560,13 @@ def extract(field, expr):
 
 def collate(expression, collation):
     """Return the clause ``expression COLLATE collation``.
-    
+
     e.g.::
-    
+
         collate(mycolumn, 'utf8_bin')
-    
+
     produces::
-    
+
         mycolumn COLLATE utf8_bin
 
     """
@@ -712,13 +712,13 @@ def alias(selectable, name=None):
     When an :class:`.Alias` is created from a :class:`.Table` object,
     this has the effect of the table being rendered
     as ``tablename AS aliasname`` in a SELECT statement.
-    
+
     For :func:`.select` objects, the effect is that of creating a named
     subquery, i.e. ``(select ...) AS aliasname``.
 
     The ``name`` parameter is optional, and provides the name
     to use in the rendered SQL.  If blank, an "anonymous" name
-    will be deterministically generated at compile time.  
+    will be deterministically generated at compile time.
     Deterministic means the name is guaranteed to be unique against
     other constructs used in the same statement, and will also be the
     same name for each successive compilation of the same statement
@@ -842,10 +842,10 @@ def column(text, type_=None):
     :class:`~sqlalchemy.schema.Column` object.  It is often used directly
     within :func:`~.expression.select` constructs or with lightweight :func:`~.expression.table`
     constructs.
-    
+
     Note that the :func:`~.expression.column` function is not part of
     the ``sqlalchemy`` namespace.  It must be imported from the ``sql`` package::
-    
+
         from sqlalchemy.sql import table, column
 
     :param text: the name of the column.  Quoting rules will be applied 
@@ -870,7 +870,7 @@ def literal_column(text, type_=None):
     (such as, '+' means string concatenation or numerical addition based on
     the type).
 
-    :param text: the text of the expression; can be any SQL expression.  
+    :param text: the text of the expression; can be any SQL expression.
       Quoting rules will not be applied. To specify a column-name expression
       which should be subject to quoting rules, use the :func:`column`
       function.
@@ -888,18 +888,18 @@ def table(name, *columns):
     The object returned is an instance of :class:`.TableClause`, which represents the
     "syntactical" portion of the schema-level :class:`~.schema.Table` object. 
     It may be used to construct lightweight table constructs. 
-    
+
     Note that the :func:`~.expression.table` function is not part of
     the ``sqlalchemy`` namespace.  It must be imported from the ``sql`` package::
-    
+
         from sqlalchemy.sql import table, column
-    
+
     :param name: Name of the table.
-    
+
     :param columns: A collection of :func:`~.expression.column` constructs.
-    
+
     See :class:`.TableClause` for further examples.
-    
+
     """
     return TableClause(name, *columns)
 
@@ -1061,15 +1061,15 @@ def text(text, bind=None, *args, **kwargs):
 
 def over(func, partition_by=None, order_by=None):
     """Produce an OVER clause against a function.
-    
+
     Used against aggregate or so-called "window" functions,
     for database backends that support window functions.
-    
+
     E.g.::
-    
+
         from sqlalchemy import over
         over(func.row_number(), order_by='x')
-        
+
     Would produce "ROW_NUMBER() OVER(ORDER BY x)".
 
     :param func: a :class:`.FunctionElement` construct, typically
@@ -1080,10 +1080,10 @@ def over(func, partition_by=None, order_by=None):
     :param order_by: a column element or string, or a list
      of such, that will be used as the ORDER BY clause
      of the OVER construct.
-     
+
     This function is also available from the :attr:`~.expression.func`
     construct itself via the :meth:`.FunctionElement.over` method.
-    
+
     New in 0.7.
 
     """
@@ -1154,7 +1154,7 @@ func = _FunctionGenerator()
 
    The element is a column-oriented SQL element like any other, and is
    used in that way::
-   
+
         >>> print select([func.count(table.c.id)])
         SELECT count(sometable.id) FROM sometable
 
@@ -1170,7 +1170,7 @@ func = _FunctionGenerator()
 
         >>> print func.stats.yield_curve(5, 10)
         stats.yield_curve(:yield_curve_1, :yield_curve_2)
-    
+
    SQLAlchemy can be made aware of the return type of functions to enable
    type-specific lexical and result-based behavior. For example, to ensure
    that a string-based function returns a Unicode value and is similarly
@@ -1186,13 +1186,13 @@ func = _FunctionGenerator()
    functions.  The object can also be passed the :meth:`~.Connectable.execute`
    method of a :class:`.Connection` or :class:`.Engine`, where it will be
    wrapped inside of a SELECT statement first::
-   
+
         print connection.execute(func.current_timestamp()).scalar()
-        
+
    A function can also be "bound" to a :class:`.Engine` or :class:`.Connection`
    using the ``bind`` keyword argument, providing an execute() as well
    as a scalar() method::
-   
+
         myfunc = func.current_timestamp(bind=some_engine)
         print myfunc.scalar()
 
@@ -1539,7 +1539,7 @@ class ClauseElement(Visitable):
         The given clone function should be used, which may be applying
         additional transformations to the element (i.e. replacement
         traversal, cloned traversal, annotations).
-        
+
         """
         pass
 
@@ -1713,10 +1713,10 @@ class _Immutable(object):
 class _CompareMixin(ColumnOperators):
     """Defines comparison and math operations for :class:`.ClauseElement`
     instances.
-    
+
     See :class:`.ColumnOperators` and :class:`.Operators` for descriptions 
     of all operations.
-    
+
     """
 
     def __compare(self, op, obj, negate=None, reverse=False,
@@ -2249,6 +2249,7 @@ class FromClause(Selectable):
     _hide_froms = []
     quote = None
     schema = None
+    _memoized_property = util.group_expirable_memoized_property(["_columns"]) 
 
     def count(self, whereclause=None, **params):
         """return a SELECT COUNT generated against this
@@ -2283,12 +2284,12 @@ class FromClause(Selectable):
 
     def alias(self, name=None):
         """return an alias of this :class:`.FromClause`.
-        
+
         This is shorthand for calling::
-        
+
             from sqlalchemy import alias
             a = alias(self, name=name)
-            
+
         See :func:`~.expression.alias` for details.
 
         """
@@ -2401,11 +2402,9 @@ class FromClause(Selectable):
     def _reset_exported(self):
         """delete memoized collections when a FromClause is cloned."""
 
-        for name in 'primary_key', '_columns', 'columns', \
-                'foreign_keys', 'locate_all_froms':
-            self.__dict__.pop(name, None)
+        self._memoized_property.expire_instance(self)
 
-    @util.memoized_property
+    @_memoized_property
     def columns(self):
         """Return the collection of Column objects contained by this
         FromClause."""
@@ -2415,7 +2414,7 @@ class FromClause(Selectable):
             self._populate_column_collection()
         return self._columns.as_immutable()
 
-    @util.memoized_property
+    @_memoized_property
     def primary_key(self):
         """Return the collection of Column objects which comprise the
         primary key of this FromClause."""
@@ -2424,7 +2423,7 @@ class FromClause(Selectable):
         self._populate_column_collection()
         return self.primary_key
 
-    @util.memoized_property
+    @_memoized_property
     def foreign_keys(self):
         """Return the collection of ForeignKey objects which this
         FromClause references."""
@@ -2616,21 +2615,21 @@ class Executable(_Generative):
     def execution_options(self, **kw):
         """ Set non-SQL options for the statement which take effect during
         execution.
-        
+
         Execution options can be set on a per-statement or 
         per :class:`.Connection` basis.   Additionally, the 
         :class:`.Engine` and ORM :class:`~.orm.query.Query` objects provide access
         to execution options which they in turn configure upon connections.
-        
+
         The :meth:`execution_options` method is generative.  A new 
         instance of this statement is returned that contains the options::
-        
+
             statement = select([table.c.x, table.c.y])
             statement = statement.execution_options(autocommit=True)
-        
+
         Note that only a subset of possible execution options can be applied
         to a statement - these include "autocommit" and "stream_results",
-        but not "isolation_level" or "compiled_cache".  
+        but not "isolation_level" or "compiled_cache".
         See :meth:`.Connection.execution_options` for a full list of 
         possible options.
 
@@ -2678,7 +2677,7 @@ class Executable(_Generative):
     def bind(self):
         """Returns the :class:`.Engine` or :class:`.Connection` to 
         which this :class:`.Executable` is bound, or None if none found.
-        
+
         This is a traversal which checks locally, then
         checks among the "from" clauses of associated objects
         until a bound engine or connection is found.
@@ -2998,9 +2997,9 @@ class FunctionElement(Executable, ColumnElement, FromClause):
     @property
     def columns(self):
         """Fulfill the 'columns' contrct of :class:`.ColumnElement`.
-        
+
         Returns a single-element list consisting of this object.
-        
+
         """
         return [self]
 
@@ -3008,29 +3007,29 @@ class FunctionElement(Executable, ColumnElement, FromClause):
     def clauses(self):
         """Return the underlying :class:`.ClauseList` which contains
         the arguments for this :class:`.FunctionElement`.
-        
+
         """
         return self.clause_expr.element
 
     def over(self, partition_by=None, order_by=None):
         """Produce an OVER clause against this function.
-        
+
         Used against aggregate or so-called "window" functions,
         for database backends that support window functions.
-        
+
         The expression::
-        
+
             func.row_number().over(order_by='x')
-            
+
         is shorthand for::
-        
+
             from sqlalchemy import over
             over(func.row_number(), order_by='x')
 
         See :func:`~.expression.over` for a full description.
-        
+
         New in 0.7.
-        
+
         """
         return over(self, partition_by=partition_by, order_by=order_by)
 
@@ -3049,11 +3048,11 @@ class FunctionElement(Executable, ColumnElement, FromClause):
     def select(self):
         """Produce a :func:`~.expression.select` construct 
         against this :class:`.FunctionElement`.
-        
+
         This is shorthand for::
-        
+
             s = select([function_element])
-            
+
         """
         s = select([self])
         if self._execution_options:
@@ -3063,28 +3062,28 @@ class FunctionElement(Executable, ColumnElement, FromClause):
     def scalar(self):
         """Execute this :class:`.FunctionElement` against an embedded
         'bind' and return a scalar value.
-        
+
         This first calls :meth:`~.FunctionElement.select` to 
         produce a SELECT construct.
-        
+
         Note that :class:`.FunctionElement` can be passed to 
         the :meth:`.Connectable.scalar` method of :class:`.Connection`
         or :class:`.Engine`.
-        
+
         """
         return self.select().execute().scalar()
 
     def execute(self):
         """Execute this :class:`.FunctionElement` against an embedded
         'bind'.
-        
+
         This first calls :meth:`~.FunctionElement.select` to 
         produce a SELECT construct.
-        
+
         Note that :class:`.FunctionElement` can be passed to 
         the :meth:`.Connectable.execute` method of :class:`.Connection`
         or :class:`.Engine`.
-        
+
         """
         return self.select().execute()
 
@@ -3095,20 +3094,20 @@ class FunctionElement(Executable, ColumnElement, FromClause):
 
 class Function(FunctionElement):
     """Describe a named SQL function.
-    
+
     See the superclass :class:`.FunctionElement` for a description
     of public methods.
-    
+
     """
 
     __visit_name__ = 'function'
 
     def __init__(self, name, *clauses, **kw):
         """Construct a :class:`.Function`.
-        
+
         The :attr:`.func` construct is normally used to construct 
         new :class:`.Function` instances.
-        
+
         """
         self.packagenames = kw.pop('packagenames', None) or []
         self.name = name
@@ -3343,11 +3342,11 @@ class Join(FromClause):
 
     def __init__(self, left, right, onclause=None, isouter=False):
         """Construct a new :class:`.Join`.
-        
+
         The usual entrypoint here is the :func:`~.expression.join`
         function or the :meth:`.FromClause.join` method of any
         :class:`.FromClause` object.
-        
+
         """
         self.left = _literal_as_text(left)
         self.right = _literal_as_text(right).self_group()
@@ -3405,15 +3404,15 @@ class Join(FromClause):
 
     def select(self, whereclause=None, fold_equivalents=False, **kwargs):
         """Create a :class:`.Select` from this :class:`.Join`.
-        
+
         The equivalent long-hand form, given a :class:`.Join` object
         ``j``, is::
-        
+
             from sqlalchemy import select
             j = select([j.left, j.right], **kw).\\
                         where(whereclause).\\
                         select_from(j)
-            
+
         :param whereclause: the WHERE criterion that will be sent to 
           the :func:`select()` function
 
@@ -3442,7 +3441,7 @@ class Join(FromClause):
 
     def alias(self, name=None):
         """return an alias of this :class:`.Join`.
-        
+
         Used against a :class:`.Join` object,
         :meth:`~.Join.alias` calls the :meth:`~.Join.select`
         method first so that a subquery against a 
@@ -3451,10 +3450,10 @@ class Join(FromClause):
         ``correlate`` flag set to ``False`` and will not
         auto-correlate inside an enclosing :func:`~expression.select`
         construct.
-        
+
         The equivalent long-hand form, given a :class:`.Join` object
         ``j``, is::
-        
+
             from sqlalchemy import select, alias
             j = alias(
                 select([j.left, j.right]).\\
@@ -3466,7 +3465,7 @@ class Join(FromClause):
 
         See :func:`~.expression.alias` for further details on 
         aliases.
-        
+
         """
         return self.select(use_labels=True, correlate=False).alias(name)
 
@@ -3649,13 +3648,13 @@ class _FromGrouping(FromClause):
 
 class _Over(ColumnElement):
     """Represent an OVER clause.
-    
+
     This is a special operator against a so-called 
     "window" function, as well as any aggregate function,
     which produces results relative to the result set
     itself.  It's supported only by certain database
     backends.
-    
+
     """
     __visit_name__ = 'over'
 
@@ -3766,31 +3765,31 @@ class ColumnClause(_Immutable, ColumnElement):
     This includes columns associated with tables, aliases and select
     statements, but also any arbitrary text.  May or may not be bound
     to an underlying :class:`.Selectable`.
-    
+
     :class:`.ColumnClause` is constructed by itself typically via
     the :func:`~.expression.column` function.  It may be placed directly
     into constructs such as :func:`.select` constructs::
-    
+
         from sqlalchemy.sql import column, select
-        
+
         c1, c2 = column("c1"), column("c2")
         s = select([c1, c2]).where(c1==5)
-    
+
     There is also a variant on :func:`~.expression.column` known
     as :func:`~.expression.literal_column` - the difference is that 
     in the latter case, the string value is assumed to be an exact
     expression, rather than a column name, so that no quoting rules
     or similar are applied::
-    
+
         from sqlalchemy.sql import literal_column, select
-        
+
         s = select([literal_column("5 + 7")])
-    
+
     :class:`.ColumnClause` can also be used in a table-like 
     fashion by combining the :func:`~.expression.column` function 
     with the :func:`~.expression.table` function, to produce
     a "lightweight" form of table metadata::
-    
+
         from sqlalchemy.sql import table, column
 
         user = table("user",
@@ -3798,7 +3797,7 @@ class ColumnClause(_Immutable, ColumnElement):
                 column("name"),
                 column("description"),
         )
-    
+
     The above construct can be created in an ad-hoc fashion and is
     not associated with any :class:`.schema.MetaData`, unlike it's
     more full fledged :class:`.schema.Table` counterpart.
@@ -3821,16 +3820,32 @@ class ColumnClause(_Immutable, ColumnElement):
 
     onupdate = default = server_default = server_onupdate = None
 
+    _memoized_property = util.group_expirable_memoized_property() 
+
     def __init__(self, text, selectable=None, type_=None, is_literal=False):
         self.key = self.name = text
         self.table = selectable
         self.type = sqltypes.to_instance(type_)
         self.is_literal = is_literal
 
-    @util.memoized_property
+    def _get_table(self):
+        return self.__dict__['table']
+    def _set_table(self, table):
+        if '_from_objects' in self.__dict__:
+            util.warn("%s being associated with %s object after "
+                        "the %s has already been used in a SQL "
+                        "generation; previously generated "
+                        "constructs may contain stale state." % 
+                        (type(table), type(self), type(self)))
+        self._memoized_property.expire_instance(self)
+        self.__dict__['table'] = table
+    table = property(_get_table, _set_table)
+
+    @_memoized_property
     def _from_objects(self):
-        if self.table is not None:
-            return [self.table]
+        t = self.table
+        if t is not None:
+            return [t]
         else:
             return []
 
@@ -3842,26 +3857,27 @@ class ColumnClause(_Immutable, ColumnElement):
         return self.name.encode('ascii', 'backslashreplace')
         # end Py2K
 
-    @util.memoized_property
+    @_memoized_property
     def _label(self):
+        t = self.table
         if self.is_literal:
             return None
 
-        elif self.table is not None and self.table.named_with_column:
-            if getattr(self.table, 'schema', None):
-                label = self.table.schema.replace('.', '_') + "_" + \
-                            _escape_for_generated(self.table.name) + "_" + \
+        elif t is not None and t.named_with_column:
+            if getattr(t, 'schema', None):
+                label = t.schema.replace('.', '_') + "_" + \
+                            _escape_for_generated(t.name) + "_" + \
                             _escape_for_generated(self.name)
             else:
-                label = _escape_for_generated(self.table.name) + "_" + \
+                label = _escape_for_generated(t.name) + "_" + \
                             _escape_for_generated(self.name)
 
             # ensure the label name doesn't conflict with that
             # of an existing column
-            if label in self.table.c:
+            if label in t.c:
                 _label = label
                 counter = 1
-                while _label in self.table.c:
+                while _label in t.c:
                     _label = label + "_" + str(counter)
                     counter += 1
                 label = _label
@@ -3908,26 +3924,26 @@ class ColumnClause(_Immutable, ColumnElement):
 
 class TableClause(_Immutable, FromClause):
     """Represents a minimal "table" construct.
-    
+
     The constructor for :class:`.TableClause` is the
     :func:`~.expression.table` function.   This produces 
     a lightweight table object that has only a name and a 
     collection of columns, which are typically produced
     by the :func:`~.expression.column` function::
-    
+
         from sqlalchemy.sql import table, column
-        
+
         user = table("user",
                 column("id"),
                 column("name"),
                 column("description"),
         )
-        
+
     The :class:`.TableClause` construct serves as the base for
     the more commonly used :class:`~.schema.Table` object, providing
     the usual set of :class:`~.expression.FromClause` services including
     the ``.c.`` collection and statement generation methods.
-    
+
     It does **not** provide all the additional schema-level services
     of :class:`~.schema.Table`, including constraints, references to other 
     tables, or support for :class:`.MetaData`-level services.  It's useful
@@ -4291,9 +4307,9 @@ class Select(_SelectBase):
     """Represents a ``SELECT`` statement.
 
     See also:
-    
+
     :func:`~.expression.select` - the function which creates a :class:`.Select` object.
-    
+
     :ref:`coretutorial_selecting` - Core Tutorial description of :func:`.select`.
 
     """
@@ -4303,6 +4319,9 @@ class Select(_SelectBase):
     _prefixes = ()
     _hints = util.immutabledict()
     _distinct = False
+    _from_cloned = None
+
+    _memoized_property = _SelectBase._memoized_property
 
     def __init__(self, 
                 columns, 
@@ -4344,7 +4363,12 @@ class Select(_SelectBase):
                             ]
 
         self._correlate = set()
-        self._froms = util.OrderedSet()
+        if from_obj is not None:
+            self._from_obj = util.OrderedSet(
+                                _literal_as_text(f) 
+                                for f in util.to_list(from_obj))
+        else:
+            self._from_obj = util.OrderedSet()
 
         try:
             cols_present = bool(columns)
@@ -4359,24 +4383,14 @@ class Select(_SelectBase):
                 if isinstance(c, _ScalarSelect):
                     c = c.self_group(against=operators.comma_op)
                 self._raw_columns.append(c)
-
-            self._froms.update(_from_objects(*self._raw_columns))
         else:
             self._raw_columns = []
 
         if whereclause is not None:
             self._whereclause = _literal_as_text(whereclause)
-            self._froms.update(_from_objects(self._whereclause))
         else:
             self._whereclause = None
 
-        if from_obj is not None:
-            for f in util.to_list(from_obj):
-                if _is_literal(f):
-                    self._froms.add(_TextClause(f))
-                else:
-                    self._froms.add(f)
-
         if having is not None:
             self._having = _literal_as_text(having)
         else:
@@ -4387,6 +4401,27 @@ class Select(_SelectBase):
 
         _SelectBase.__init__(self, **kwargs)
 
+    @_memoized_property
+    def _froms(self):
+        froms = []
+        seen = set()
+        translate = self._from_cloned
+
+        def add(items):
+            for item in items:
+                if translate and item in translate:
+                    item = translate[item]
+                if not seen.intersection(item._cloned_set):
+                    froms.append(item)
+                seen.update(item._cloned_set)
+
+        add(_from_objects(*self._raw_columns))
+        if self._whereclause is not None:
+            add(_from_objects(self._whereclause))
+        add(self._from_obj)
+
+        return froms
+
     def _get_display_froms(self, existing_froms=None):
         """Return the full list of 'from' clauses to be displayed.
 
@@ -4398,17 +4433,17 @@ class Select(_SelectBase):
         """
         froms = self._froms
 
-        toremove = itertools.chain(*[f._hide_froms for f in froms])
+        toremove = set(itertools.chain(*[f._hide_froms for f in froms]))
         if toremove:
-            froms = froms.difference(toremove)
+            froms = [f for f in froms if f not in toremove]
 
         if len(froms) > 1 or self._correlate:
             if self._correlate:
-                froms = froms.difference(_cloned_intersection(froms,
-                        self._correlate))
+                froms = [f for f in froms if f not in _cloned_intersection(froms,
+                        self._correlate)]
             if self._should_correlate and existing_froms:
-                froms = froms.difference(_cloned_intersection(froms,
-                        existing_froms))
+                froms = [f for f in froms if f not in _cloned_intersection(froms,
+                        existing_froms)]
 
                 if not len(froms):
                     raise exc.InvalidRequestError("Select statement '%s"
@@ -4468,7 +4503,7 @@ class Select(_SelectBase):
                     "Call as_scalar() on this Select object "
                     "to return a 'scalar' version of this Select.")
 
-    @util.memoized_instancemethod
+    @_memoized_property.method
     def locate_all_froms(self):
         """return a Set of all FromClause elements referenced by this Select.
 
@@ -4477,7 +4512,7 @@ class Select(_SelectBase):
         actually be rendered.
 
         """
-        return self._froms.union(_from_objects(*list(self._froms)))
+        return self._froms + list(_from_objects(*self._froms))
 
     @property
     def inner_columns(self):
@@ -4497,17 +4532,53 @@ class Select(_SelectBase):
         return False
 
     def _copy_internals(self, clone=_clone, **kw):
-        self._reset_exported()
-        from_cloned = dict((f, clone(f, **kw))
-                           for f in self._froms.union(self._correlate))
-        self._froms = util.OrderedSet(from_cloned[f] for f in self._froms)
-        self._correlate = set(from_cloned[f] for f in self._correlate)
+
+        # Select() object has been cloned and probably adapted by the
+        # given clone function.  Apply the cloning function to internal
+        # objects
+
+        # 1.  Fill up the persistent "_from_obj" collection with a baked
+        # "_froms" collection.  "_froms" gets cleared anytime a
+        # generative call like where(), select_from() occurs.  _from_obj
+        # will keep a persistent version of it.  Whether or not this is
+        # done affects a pair of tests in test.sql.test_generative.
+        self._from_obj = self._from_obj.union(self._froms)
+
+        # 2. keep a dictionary of the froms we've cloned, and what
+        # they've become.  This is consulted later when we derive
+        # additional froms from "whereclause" and the columns clause,
+        # which may still reference the uncloned parent table
+        self._from_cloned = from_cloned = dict((f, clone(f, **kw))
+                for f in self._from_obj)
+
+        # 3. update persistent _from_obj with the cloned versions.
+        self._from_obj = util.OrderedSet(from_cloned[f] for f in
+                self._from_obj)
+
+        # the _correlate collection is done separately, what can happen
+        # here is the same item is _correlate as in _from_obj but the
+        # _correlate version has an annotation on it - (specifically
+        # RelationshipProperty.Comparator._criterion_exists() does
+        # this). Also keep _correlate liberally open with it's previous
+        # contents, as this set is used for matching, not rendering.
+        self._correlate = set(clone(f) for f in
+                              self._correlate).union(self._correlate)
+
+        # 4. clone other things.   The difficulty here is that Column
+        # objects are not actually cloned, and refer to their original
+        # .table, resulting in the wrong "from" parent after a clone
+        # operation.  Hence _from_cloned and _from_obj supercede what is
+        # present here.
         self._raw_columns = [clone(c, **kw) for c in self._raw_columns]
         for attr in '_whereclause', '_having', '_order_by_clause', \
             '_group_by_clause':
             if getattr(self, attr) is not None:
                 setattr(self, attr, clone(getattr(self, attr), **kw))
 
+        # erase exported column list, _froms collection,
+        # etc.
+        self._reset_exported()
+
     def get_children(self, column_collections=True, **kwargs):
         """return child elements as per the ClauseElement specification."""
 
@@ -4524,14 +4595,7 @@ class Select(_SelectBase):
             added to its columns clause.
 
         """
-
-        column = _literal_as_column(column)
-
-        if isinstance(column, _ScalarSelect):
-            column = column.self_group(against=operators.comma_op)
-
-        self._raw_columns = self._raw_columns + [column]
-        self._froms = self._froms.union(_from_objects(column))
+        self.append_column(column)
 
     @_generative
     def with_only_columns(self, columns):
@@ -4539,7 +4603,7 @@ class Select(_SelectBase):
             with the given columns.
 
         """
-
+        self._reset_exported()
         self._raw_columns = [
                 isinstance(c, _ScalarSelect) and 
                 c.self_group(against=operators.comma_op) or c
@@ -4571,7 +4635,7 @@ class Select(_SelectBase):
         :param \*expr: optional column expressions.  When present,
          the Postgresql dialect will render a ``DISTINCT ON (<expressions>>)``
          construct.
-         
+
         """
         if expr:
             expr = [_literal_as_text(e) for e in expr]
@@ -4588,13 +4652,13 @@ class Select(_SelectBase):
         expressions, typically strings, to the start of its columns clause, 
         not using any commas.   In particular is useful for MySQL
         keywords.
-        
+
         e.g.::
-            
+
              select(['a', 'b']).prefix_with('HIGH_PRIORITY', 
                                     'SQL_SMALL_RESULT', 
                                     'ALL')
-        
+
         Would render::
 
             SELECT HIGH_PRIORITY SQL_SMALL_RESULT ALL a, b
@@ -4608,9 +4672,8 @@ class Select(_SelectBase):
         """return a new select() construct with the given FROM expression
         applied to its list of FROM objects.
 
-         """
-        fromclause = _literal_as_text(fromclause)
-        self._froms = self._froms.union([fromclause])
+        """
+        self.append_from(fromclause)
 
     @_generative
     def correlate(self, *fromclauses):
@@ -4647,14 +4710,13 @@ class Select(_SelectBase):
         select() construct.
 
         """
+        self._reset_exported()
         column = _literal_as_column(column)
 
         if isinstance(column, _ScalarSelect):
             column = column.self_group(against=operators.comma_op)
 
         self._raw_columns = self._raw_columns + [column]
-        self._froms = self._froms.union(_from_objects(column))
-        self._reset_exported()
 
     def append_prefix(self, clause):
         """append the given columns clause prefix expression to this select()
@@ -4671,8 +4733,8 @@ class Select(_SelectBase):
         The expression will be joined to existing WHERE criterion via AND.
 
         """
+        self._reset_exported()
         whereclause = _literal_as_text(whereclause)
-        self._froms = self._froms.union(_from_objects(whereclause))
 
         if self._whereclause is not None:
             self._whereclause = and_(self._whereclause, whereclause)
@@ -4696,11 +4758,9 @@ class Select(_SelectBase):
         FROM clause.
 
         """
-        if _is_literal(fromclause):
-            fromclause = _TextClause(fromclause)
-
-        self._froms = self._froms.union([fromclause])
-
+        self._reset_exported()
+        fromclause = _literal_as_text(fromclause)
+        self._from_obj = self._from_obj.union([fromclause])
 
     def _populate_column_collection(self):
         for c in self.inner_columns:
@@ -4783,7 +4843,7 @@ class Select(_SelectBase):
 
 class UpdateBase(Executable, ClauseElement):
     """Form the base for ``INSERT``, ``UPDATE``, and ``DELETE`` statements.
-    
+
     """
 
     __visit_name__ = 'update_base'
@@ -4803,11 +4863,11 @@ class UpdateBase(Executable, ClauseElement):
 
     def params(self, *arg, **kw):
         """Set the parameters for the statement.
-        
+
         This method raises ``NotImplementedError`` on the base class,
         and is overridden by :class:`.ValuesBase` to provide the
         SET/VALUES clause of UPDATE and INSERT.
-        
+
         """
         raise NotImplementedError(
             "params() is not supported for INSERT/UPDATE/DELETE statements."
@@ -4817,7 +4877,7 @@ class UpdateBase(Executable, ClauseElement):
     def bind(self):
         """Return a 'bind' linked to this :class:`.UpdateBase`
         or a :class:`.Table` associated with it.
-        
+
         """
         return self._bind or self.table.bind
 
@@ -4896,7 +4956,7 @@ class ValuesBase(UpdateBase):
         :param \*args: A single dictionary can be sent as the first positional
             argument. This allows non-string based keys, such as Column
             objects, to be used::
-            
+
                 users.insert().values({users.c.name : "some name"})
 
                 users.update().where(users.c.id==5).values({users.c.name : "some name"})
@@ -4919,9 +4979,9 @@ class Insert(ValuesBase):
     """Represent an INSERT construct.
 
     The :class:`.Insert` object is created using the :func:`~.expression.insert()` function.
-    
+
     See also:
-    
+
     :ref:`coretutorial_insert_expressions`
 
     """
index 453cafd830f3d7dbe034831685a487af093326d0..4dd9a52706bc4ab8c8a6809abc381b7347dac6c1 100644 (file)
@@ -496,8 +496,10 @@ def reset_memoized(instance, name):
 class group_expirable_memoized_property(object):
     """A family of @memoized_properties that can be expired in tandem."""
 
-    def __init__(self):
+    def __init__(self, attributes=()):
         self.attributes = []
+        if attributes:
+            self.attributes.extend(attributes)
 
     def expire_instance(self, instance):
         """Expire all memoized properties for *instance*."""
@@ -509,6 +511,10 @@ class group_expirable_memoized_property(object):
         self.attributes.append(fn.__name__)
         return memoized_property(fn)
 
+    def method(self, fn):
+        self.attributes.append(fn.__name__)
+        return memoized_instancemethod(fn)
+
 class importlater(object):
     """Deferred import object.
 
index 2560d228438fd1c75b5b5a46776d51ebdbfc8950..94dde71f84c3b584e59ea3272feed18f01116f7c 100644 (file)
@@ -186,13 +186,6 @@ def _produce_test(select_type):
                 eq_(sess.query(Person).all(), all_employees)
             self.assert_sql_count(testing.db, go, {'':14, 'Polymorphic':9}.get(select_type, 10))
 
-        def test_foo(self):
-            sess = create_session()
-
-            def go():
-                eq_(sess.query(Person).options(subqueryload(Engineer.machines)).all(), all_employees)
-            self.assert_sql_count(testing.db, go, {'':14, 'Unions':8, 'Polymorphic':7}.get(select_type, 8))
-
         def test_primary_eager_aliasing(self):
             sess = create_session()
 
index 7e4a92de1934ff0ce3ccdf378328b46863fd9e27..97849f845b35fb8ed9c726ac3194bf76ce9f3dac 100644 (file)
@@ -60,7 +60,6 @@ class SelectableNoFromsTest(fixtures.MappedTest, AssertsCompiledSQL):
 
         subset_select = select([common.c.id, common.c.data]).alias()
         subset_mapper = mapper(Subset, subset_select)
-
         sess = Session(bind=testing.db)
         sess.add(Subset(data=1))
         sess.flush()
index 82d018af15157b79f6259b75d1c47ec5bfb6b155..9c1f44e1a0b75a177b5827dbec0a856eaaae919e 100644 (file)
@@ -64,16 +64,16 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled
 
         assert s1.corresponding_column(scalar_select) is s1.c.foo
         assert s2.corresponding_column(scalar_select) is s2.c.foo
-    
+
     def test_label_grouped_still_corresponds(self):
         label = select([table1.c.col1]).label('foo')
         label2 = label.self_group()
-        
+
         s1 = select([label])
         s2 = select([label2])
         assert s1.corresponding_column(label) is s1.c.foo
         assert s2.corresponding_column(label) is s2.c.foo
-        
+
     def test_direct_correspondence_on_labels(self):
         # this test depends on labels being part
         # of the proxy set to get the right result
@@ -376,6 +376,117 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled
             [s.c.col1, s.corresponding_column(c2)]
         )
 
+    def test_from_list_deferred_constructor(self):
+        c1 = Column('c1', Integer)
+        c2 = Column('c2', Integer)
+
+        s = select([c1])
+
+        t = Table('t', MetaData(), c1, c2)
+
+        eq_(c1._from_objects, [t])
+        eq_(c2._from_objects, [t])
+
+        self.assert_compile(select([c1]), 
+                    "SELECT t.c1 FROM t")
+        self.assert_compile(select([c2]), 
+                    "SELECT t.c2 FROM t")
+
+    def test_from_list_deferred_whereclause(self):
+        c1 = Column('c1', Integer)
+        c2 = Column('c2', Integer)
+
+        s = select([c1]).where(c1==5)
+
+        t = Table('t', MetaData(), c1, c2)
+
+        eq_(c1._from_objects, [t])
+        eq_(c2._from_objects, [t])
+
+        self.assert_compile(select([c1]), 
+                    "SELECT t.c1 FROM t")
+        self.assert_compile(select([c2]), 
+                    "SELECT t.c2 FROM t")
+
+    def test_from_list_deferred_fromlist(self):
+        m = MetaData()
+        t1 = Table('t1', m, Column('x', Integer))
+
+        c1 = Column('c1', Integer)
+        s = select([c1]).where(c1==5).select_from(t1)
+
+        t2 = Table('t2', MetaData(), c1)
+
+        eq_(c1._from_objects, [t2])
+
+        self.assert_compile(select([c1]), 
+                    "SELECT t2.c1 FROM t2")
+
+    def test_from_list_deferred_cloning(self):
+        c1 = Column('c1', Integer)
+        c2 = Column('c2', Integer)
+
+        s = select([c1])
+        s2 = select([c2])
+        s3 = sql_util.ClauseAdapter(s).traverse(s2)
+
+        # the adaptation process needs the full
+        # FROM list so can't avoid the warning on
+        # this one
+        assert_raises_message(
+            exc.SAWarning,
+            r"\<class 'sqlalchemy.schema.Table'\> being associated ",
+            Table, 't', MetaData(), c1
+        )
+
+    def test_from_list_warning_against_existing(self):
+        c1 = Column('c1', Integer)
+        s = select([c1])
+
+        # force a compile.  
+        eq_(str(s), "SELECT c1")
+
+        # this will emit a warning
+        assert_raises_message(
+            exc.SAWarning,
+            r"\<class 'sqlalchemy.schema.Table'\> being associated "
+            r"with \<class 'sqlalchemy.schema.Column'\> object after "
+            r"the \<class 'sqlalchemy.schema.Column'\> has already "
+            r"been used in a SQL generation; previously "
+            r"generated constructs may contain stale state.",
+            Table, 't', MetaData(), c1
+        )
+
+    def test_from_list_recovers_after_warning(self):
+        c1 = Column('c1', Integer)
+        c2 = Column('c2', Integer)
+
+        s = select([c1])
+
+        # force a compile.  
+        eq_(str(s), "SELECT c1")
+
+        @testing.emits_warning()
+        def go():
+            return Table('t', MetaData(), c1, c2)
+        t = go()
+
+        eq_(c1._from_objects, [t])
+        eq_(c2._from_objects, [t])
+
+        # 's' has been baked.  Can't afford
+        # not caching select._froms.
+        # hopefully the warning will clue the user
+        self.assert_compile(s, "SELECT t.c1")
+        self.assert_compile(select([c1]), "SELECT t.c1 FROM t")
+        self.assert_compile(select([c2]), "SELECT t.c2 FROM t")
+
+    def test_label_gen_resets_on_table(self):
+        c1 = Column('c1', Integer)
+        eq_(c1._label, "c1")
+        Table('t1', MetaData(), c1)
+        eq_(c1._label, "t1_c1")
+
 class AnonLabelTest(fixtures.TestBase):
     """Test behaviors that we hope to change with [ticket:2168]."""