can't get it *exactly* as you have it without presenting some changes
to internals or other not simple patching.   But I can get it pretty
close.   Also why do we want to call with_expression(cls.thing()), and
not just cls.thing() or cls.thing.using(params)?

anyway here's one that looks like:

categories = s.query(Category).options(
    with_expression(Category.number_of_acknowledged_posts, 1)
)

think of it like functools.partial() as the arguments follow the attribute.

the above use can probably be added as a @query_expression.expression
as you describe which the built-in with_expression() could look for,
though people are already so confused by hybrids as it is, maybe you
can spend some time w/ this to see if it's worth it

see below

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
import sqlalchemy as sa


def with_expression(attr, *arg, **kw):
    expr = attr.info['using'](attr.class_, *arg, **kw)
    return sa.orm.with_expression(attr, expr)


def query_expression(fn):
    attr = sa.orm.query_expression()
    attr.info['using'] = fn
    return attr

Base = declarative_base()


class Post(Base):
    __tablename__ = 'post'
    id = Column(Integer, primary_key=True)
    text = Column(String)
    category_id = Column(Integer, ForeignKey('category.id'))
    category = relationship("Category")


class AcknowledgedPost(Base):
    __tablename__ = 'acknowledged_post'
    id = Column(Integer, primary_key=True)
    post_id = Column(Integer, ForeignKey('post.id'))
    customer_id = Column(Integer)

    post = relationship("Post", backref="acks")


class Category(Base):
    __tablename__ = 'category'
    id = Column(Integer, primary_key=True)
    x = Column(Integer)
    y = Column(Integer)

    @query_expression
    def number_of_acknowledged_posts(cls, customer_id):
        category_alias = sa.sql.expression.alias(cls.__table__)
        return sa.select(
            
[sa.func.count(sa.distinct(Post.id)).label('number_of_acknowledged_posts')]
        ).select_from(
            category_alias.join(
                Post,
                Post.category_id == category_alias.c.id
            ).join(
                AcknowledgedPost,
                AcknowledgedPost.post_id == Post.id
            )
        ).where(
            sa.and_(
                AcknowledgedPost.customer_id == customer_id,
                category_alias.c.id == Category.id
            )
        )


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

cat1, cat2, cat3 = Category(), Category(), Category()

s.add_all([
    cat1, cat2, cat3,
    Post(category=cat1, acks=[AcknowledgedPost(customer_id=1)]),
    Post(category=cat1, acks=[AcknowledgedPost(customer_id=1)]),
    Post(category=cat1),
    Post(category=cat2, acks=[AcknowledgedPost(customer_id=1)]),
    Post(category=cat2, acks=[AcknowledgedPost(customer_id=1)]),
    Post(category=cat3, acks=[AcknowledgedPost(customer_id=1)]),
])
s.commit()


categories = s.query(Category).options(
    with_expression(Category.number_of_acknowledged_posts, 1)
)

for cat in categories:
    print(cat.number_of_acknowledged_posts)



On Thu, Jun 7, 2018 at 11:50 AM, Mike Bayer <[email protected]> wrote:
> On Thu, Jun 7, 2018 at 11:21 AM, Stephen Fuhry <[email protected]> wrote:
>> Hi there! I'd like to propose an idea for an enhancement to the
>> query_expression API. I have found query_expression feature (introduced in
>> 1.2) extremely useful, however its current API is rather awkward and lacks
>> any sort of conventional organizational patterns for more common, albeit
>> advanced, use cases.
>>
>> I'd also like to add that query_expression seems to all but completely
>> eliminate the need for this horrifying, yet strangely beautiful (and very
>> powerful) strategy for adding calculated "columns" to a model that Mike
>> Bayer wrote up a good 5 years ago: https://stackoverflow.com/a/17349827
>> query_expression is also far more flexible, since the expression can be
>> modified at runtime to take session data into account (such as filtering by
>> a currently logged in user_id, for example)
>>
>> Just for a refresher, here's how the current API works (from the docs):
>>
>>
>> from sqlalchemy.orm import query_expression
>>
>> class A(Base):
>>     __tablename__ = 'a'
>>     id = Column(Integer, primary_key=True)
>>     x = Column(Integer)
>>     y = Column(Integer)
>>
>>     expr = query_expression()
>>
>> from sqlalchemy.orm import with_expression
>> q = session.query(A).options(
>>     with_expression(A.expr, A.x + A.y))
>>
>>
>>
>> This is really cool - but it is also a bit chaotic - there's no obvious
>> pattern for building "canned expressions." This means that developers must
>> roll their own "canned expression" pattern (if necessary) each time they
>> implement this. Let's consider this common scenario in a multi-tenant
>> blog-like application, and how I'd prefer to solve it with an improved
>> query_expression API:
>>
>>
>> import sqlalchemy as sa
>> from sqlalchemy.orm import query_expression
>>
>> class Post(Base):
>>
>>     id = Column(Integer, primary_key=True)
>>     text = Column(String)
>>     category_id = Column(Integer, ForeignKey('category.id'))
>>
>>
>> class AcknowledgedPost(Base):
>>     id = Column(Integer, primary_key=True)
>>     post_id = Column(Integer, ForeignKey('post.id'))
>>     customer_id = Column(Integer, ForeignKey('customer.id'))
>>
>>
>> class Category(Base):
>>     __tablename__ = 'a'
>>     id = Column(Integer, primary_key=True)
>>     x = Column(Integer)
>>     y = Column(Integer)
>>
>>     @query_expression.expression
>>     def number_of_acknowledged_posts(cls, customer_id):
>>         category_alias = sa.sql.expression.alias(cls.__table__)
>>         return sa.select(
>>
>> [sa.func.count(sa.distinct(Post.id)).label('number_of_acknowledged_posts')]
>>         ).select_from(
>>             category_alias.join(
>>                 Post,
>>                 Post.category_id == category_alias.c.id
>>             ).join(
>>                 AcknowledgedPost,
>>                 AcknowledgedPost.post_id == Post.id
>>             )
>>         ).where(
>>             sa.and_(
>>                 AcknowledgedPost.customer_id == customer_id,
>>                 category_alias.c.id == Category.id
>>             )
>>         )
>>
>>
>>
>> Now the query_expression API is much more coherent, and provides an obvious
>> way to pass in parameters for scenarios like the above:
>>
>>
>> from sqlalchemy.orm import with_expression
>> categories = session.query(Category).options(
>>     with_expression(
>>         Category.number_of_acknowledged_posts(current_user.customer_id)
>>     )
>> ).all()
>>
>>>>> categories[0].number_of_acknowledged_posts
>> 22
>>
>>
>>
>> I'd be curious to know if anyone else has any better ideas for improving
>> this pattern. I'm not sure whether it's possible for it to work exactly how
>> I described off the top of my head (the whole decorator / class attribute
>> naming thing can get a little tricky) - but I think this should be at least
>> enough to start a conversation about the future of query_expression.
>
> I don't have time right now but I think you can make a little
> decorator right now that uses the existing feature to produce that
> exact effect.   If I lose track since I am very overwhelmed right now
> please remind me.
>
>
>
>
>>
>> --
>> SQLAlchemy -
>> The Python SQL Toolkit and Object Relational Mapper
>>
>> http://www.sqlalchemy.org/
>>
>> To post example code, please provide an MCVE: Minimal, Complete, and
>> Verifiable Example. See http://stackoverflow.com/help/mcve for a full
>> description.
>> ---
>> You received this message because you are subscribed to the Google Groups
>> "sqlalchemy" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to [email protected].
>> To post to this group, send email to [email protected].
>> Visit this group at https://groups.google.com/group/sqlalchemy.
>> For more options, visit https://groups.google.com/d/optout.

-- 
SQLAlchemy - 
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to