adaption of Label breaks further operations because it does not reset the _element attribute

Issue #3445 resolved
Lukas Siemon created an issue

The test case below generates the following query:

SELECT
   venue.id AS venue_id   
FROM
   venue   
WHERE
   EXISTS (
      SELECT
         1   
      FROM
         label,
         venue AS venue_alias2 
      JOIN
         venue_to_label AS venue_to_label_1 
            ON venue_alias2.id = venue_to_label_1.venue_id 
      JOIN
         label AS label_alias 
            ON label_alias.id = venue_to_label_1.label_id   
      WHERE
         venue.id = venue_alias2.id 
         AND (
            label.id = :param_1
         ) IS 1
   )

However I'd expect the subquery to use the aliased Label model, i.e. instead of "label.id = :param_1" it should say "label_alias.id = :param_1".

The problem seems to be with the column_property.

I've tested the issue with SQLAlchemy==1.0.5 Flask==0.10.1 Flask-SQLAlchemy==2.0

Minimal test case:

import unittest
from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, aliased, column_property
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = ("sqlite://")
db = SQLAlchemy(app)

Base = declarative_base()

venue_to_label = Table(
    'venue_to_label', db.metadata,
    Column('venue_id', Integer, ForeignKey('venue.id'), primary_key=True),
    Column('label_id', Integer, ForeignKey('label.id'), primary_key=True)
)
label_to_label = Table(
    'label_to_label', db.metadata,
    Column('label_id', Integer, ForeignKey('label.id'), primary_key=True),
    Column('label_id', Integer, ForeignKey('label.id'), primary_key=True)
)


class Label(db.Model):
    __tablename__ = 'label'
    id = Column(Integer, primary_key=True, nullable=False)
    is_first = column_property(id == 1)


class Venue(db.Model):
    __tablename__ = 'venue'
    id = Column(Integer, primary_key=True, nullable=False)
    labels = relationship(Label, secondary=venue_to_label)

Base.metadata.drop_all(bind=db.engine)
Base.metadata.create_all(bind=db.engine)


class TestColPropAliasBug(unittest.TestCase):

    def test_column_property_aliased_bug(self):

        query = db.session.query(Venue)

        venue_alias2 = aliased(Venue, name="venue_alias2")
        subquery = db.session.query(venue_alias2)
        label_alias = aliased(venue_alias2.labels, name="label_alias")
        subquery = subquery.join(label_alias, venue_alias2.labels)

        # relate queries
        subquery = subquery.filter(Venue.id == venue_alias2.id)

        # filter_ = label_alias.id.is_(1)  # works as expected
        filter_ = label_alias.is_first.is_(True)  # doesn't work as expected
        subquery = subquery.filter(filter_)
        query = query.filter(subquery.exists())

        print query

Comments (5)

  1. Mike Bayer repo owner

    here's a very short test:

    from sqlalchemy import *
    from sqlalchemy.orm import *
    from sqlalchemy.ext.declarative import declarative_base
    
    Base = declarative_base()
    
    
    class Label(Base):
        __tablename__ = 'label'
        id = Column(Integer, primary_key=True, nullable=False)
        is_first = column_property(id == 1)
    
    s = Session()
    
    label_alias = aliased(Label)
    q = s.query(label_alias)
    print q.filter(label_alias.is_first)
    print "-------"
    print q.filter(label_alias.is_first.self_group())
    

    here's a potential patch, needs more specific tests:

    diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
    index a178ed9..25e8357 100644
    --- a/lib/sqlalchemy/sql/elements.py
    +++ b/lib/sqlalchemy/sql/elements.py
    @@ -3103,7 +3103,8 @@ class Label(ColumnElement):
             return self.element,
    
         def _copy_internals(self, clone=_clone, anonymize_labels=False, **kw):
    -        self.element = clone(self.element, **kw)
    +        self.__dict__.pop('element', None)
    +        self._element = clone(self._element, **kw)
             self.__dict__.pop('_allow_label_resolve', None)
             if anonymize_labels:
                 self.name = self._resolve_label = _anonymous_label(
    
  2. Mike Bayer repo owner
    • Fixed a bug where clause adaption as applied to a :class:.Label object would fail to accommodate the labeled SQL expression in all cases, such that any SQL operation that made use of :meth:.Label.self_group would use the original unadapted expression. One effect of this would be that an ORM :func:.aliased construct would not fully accommodate attributes mapped by :obj:.column_property, such that the un-aliased table could leak out when the property were used in some kinds of SQL comparisons. fixes #3445

    → <<cset a9030d024196>>

  3. Log in to comment