move fold_equivalents into select()

Issue #1729 resolved
Mike Bayer repo owner created an issue
s = select([t1.c.y, t2.c.x, t2.c.z](t1.c.x,), fold_equivalents=True)
s.append_column(t3.c.x)
s.append_column(t3.c.q)

the above select would have the columns ['y', 'z', 'q']('x',), linking to that of t1, t1, t2, and t3, respectively.

lets do the same example above. Except now, t1.c.x references t3.c.x via foreign key. The column names would be the same, but now the cols would link to t3, t1, t2, and t3. This requires modifying the internal "raw_columns" collection. As far as the exported ".c." attribute, that is reset each time append_column or column is called anyway so that's not an issue here. The reason for this behavior is that we now gain the knowledge that x is really a primary key column, which is helpful when mapping to select statements.

Comments (4)

  1. Mike Bayer reporter
    • changed milestone to 0.8.0

    a more critical use case for this:

    from sqlalchemy import *
    from sqlalchemy.orm import *
    from sqlalchemy.ext.declarative import declarative_base
    
    Base= declarative_base()
    
    class Resource(Base):
        __tablename__ = "resources"
        id = Column("id", Integer, primary_key=True)
    
    class BasePrAc(Resource):
        __tablename__ = "base_products_accessories"
        _polymorphicIdentity = Column("polymorphic_identity", String(20), key="polymorphicIdentity")
        __mapper_args__ = {
            'polymorphic_on': _polymorphicIdentity,
            'polymorphic_identity': None
        }
        id = Column("id", Integer, ForeignKey("resources.id"), primary_key=True)
    
    class Product(BasePrAc):
        __tablename__ = "products"
        __mapper_args__ = {
            'polymorphic_identity': 'Product'
        }
        id = Column("id", Integer, ForeignKey("base_products_accessories.id"), primary_key=True)
    
    class Post(Base):
        __tablename__ = "posts"
        id = Column("id", Integer, primary_key=True)
        basePrAcId = Column(Integer, ForeignKey('base_products_accessories.id'))
        basePrAc = relationship("BasePrAc", uselist=False, backref=backref("_posts", collection_class=set))
    
    e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
    Base.metadata.drop_all(e)
    Base.metadata.create_all(e)
    s = Session(e)
    s.add_all([   Post(
            basePrAc=Product()
        ),
    ](
    ))
    
    sq = s.query(BasePrAc).subquery()
    q = s.query(Post).join((sq, sq.c.id == Post.basePrAcId)).all()
    print q
    

    but looking at fold_equivalents, this seems like not quite the right thing still - it's doing things based just on the name. We really want something more like reduce_columns(). Here's a patch:

    diff -r f10c2f3dc38c627c108c88f1190dd4e9c4512a94 lib/sqlalchemy/orm/query.py
    --- a/lib/sqlalchemy/orm/query.py   Sun Apr 29 18:53:29 2012 -0400
    +++ b/lib/sqlalchemy/orm/query.py   Thu May 03 20:44:57 2012 -0400
    @@ -426,7 +426,7 @@
             # the annotation not being there
             return stmt._annotate({'no_replacement_traverse': True})
    
    -    def subquery(self, name=None):
    +    def subquery(self, name=None, with_labels=False, reduce_equivalents=False):
             """return the full SELECT statement represented by this :class:`.Query`, 
             embedded within an :class:`.Alias`.
    
    @@ -444,7 +444,13 @@
    
    
             """
    -        return self.enable_eagerloads(False).statement.alias(name=name)
    +        q = self.enable_eagerloads(False)
    +        if with_labels:
    +            q = q.with_labels()
    +        q = q.statement
    +        if reduce_equivalents:
    +            q = q.reduce_columns()
    +        return q.alias(name=name)
    
         def cte(self, name=None, recursive=False):
             """Return the full SELECT statement represented by this :class:`.Query`
    diff -r f10c2f3dc38c627c108c88f1190dd4e9c4512a94 lib/sqlalchemy/sql/expression.py
    --- a/lib/sqlalchemy/sql/expression.py  Sun Apr 29 18:53:29 2012 -0400
    +++ b/lib/sqlalchemy/sql/expression.py  Thu May 03 20:44:57 2012 -0400
    @@ -4978,6 +4978,15 @@
             """
             self.append_column(column)
    
    +    def reduce_columns(self, only_synonyms=False):
    +        return self.with_only_columns(
    +                sqlutil.reduce_columns(
    +                        self.inner_columns, 
    +                        *(self._whereclause, ) + tuple(self._from_obj),
    +                        only_synonyms=only_synonyms
    +                )
    +            )
    +
         @_generative
         def with_only_columns(self, columns):
             """Return a new :func:`.select` construct with its columns 
    diff -r f10c2f3dc38c627c108c88f1190dd4e9c4512a94 lib/sqlalchemy/sql/util.py
    --- a/lib/sqlalchemy/sql/util.py    Sun Apr 29 18:53:29 2012 -0400
    +++ b/lib/sqlalchemy/sql/util.py    Thu May 03 20:44:57 2012 -0400
    @@ -575,7 +575,8 @@
         to further identify columns that are "equivalent".
    
         \**kw may specify 'ignore_nonexistent_tables' to ignore foreign keys
    -    whose tables are not yet configured.
    +    whose tables are not yet configured, or "only_synonyms" indicating
    +    columns with different names should remain.
    
         This function is primarily used to determine the most minimal "primary key"
         from a selectable, by reducing the set of primary key columns present
    @@ -583,6 +584,7 @@
    
         """
         ignore_nonexistent_tables = kw.pop('ignore_nonexistent_tables', False)
    +    only_synonyms = kw.pop('only_synonyms', False)
    
         columns = util.ordered_column_set(columns)
    
    @@ -599,7 +601,9 @@
                             continue
                         else:
                             raise
    -                if fk_col.shares_lineage(c):
    +                if fk_col.shares_lineage(c) and \
    +                    not only_synonyms or \
    +                    c.name == fk_col.name:
                         omit.add(col)
                         break
    
    @@ -609,11 +613,14 @@
                     cols = util.column_set(chain(*[for c in columns.difference(omit)](c.proxy_set)))
                     if binary.left in cols and binary.right in cols:
                         for c in columns:
    -                        if c.shares_lineage(binary.right):
    +                        if c.shares_lineage(binary.right) and \
    +                            not only_synonyms or \
    +                            c.name == binary.right.name:
                                 omit.add(c)
                                 break
             for clause in clauses:
    -            visitors.traverse(clause, {}, {'binary':visit_binary})
    +            if clause is not None:
    +                visitors.traverse(clause, {}, {'binary':visit_binary})
    
         return expression.ColumnSet(columns.difference(omit))
    

    the next issue becomes, can we just remove the fold_equivalents() functionality altogether. I think we can, we're feeding in the whereclause and the froms here so it takes into account the join condition.

  2. Log in to comment