Is there a LISTAGG WITHIN GROUP equivalent in SQLAlchemy?

╄→尐↘猪︶ㄣ 提交于 2019-12-05 21:52:16

Beginning from version 1.1 you can use FunctionElement.within_group(*order_by):

In [7]: func.listagg(column('person'), ',').within_group(column('person'))
Out[7]: <sqlalchemy.sql.elements.WithinGroup object at 0x7f2870c83080>

In [8]: print(_.compile(dialect=oracle.dialect()))
listagg(person, :listagg_1) WITHIN GROUP (ORDER BY person)

Ilja's answer did the trick for me. Here it is fully fleshed out, using SQLAlchemy 1.2.2 (I couldn't get it to work in 1.1.10, but upgrading took care of that)

from sqlalchemy import Column, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from lib import project_config
from sqlalchemy import func

db_url = 'oracle://someuser:somepassword@some_connect_string'    

Base = declarative_base()
engine = create_engine(db_url, echo=True)
Session = sessionmaker(bind=engine)
session = Session()

class MyTable(Base):
    __tablename__ = 'my_table'
    food   = Column(String(30), primary_key=True)
    person = Column(String(30), primary_key=True)

Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

session.add(MyTable(food='pizza', person='Adam'))
session.add(MyTable(food='pizza', person='Bob')) 
session.add(MyTable(food='pizza', person='Charles'))
session.add(MyTable(food='ice cream', person='Donald'))
session.add(MyTable(food='hamburger', person='Emma'))  
session.add(MyTable(food='hamburger', person='Frank'))
session.commit()

entries = session.query(
      MyTable.food,
      func.listagg(MyTable.person, ',').within_group(MyTable.person).label('people')
    ).group_by(MyTable.food).all()

[print('{}: {}'.format(entry.food, entry.people)) for entry in entries]

which prints out:

hamburger: Emma,Frank
ice cream: Donald
pizza: Adam,Bob,Charles

which is great! The only remaining mystery is why the separator character (,) is preceded by a NULL:

>>> print(entries)
[('hamburger', 'Emma\x00,Frank'), ('ice cream', 'Donald'), ('pizza', 'Adam\x00,Bob\x00,Charles')]

In fact if I change the separator in the func.listagg() to something else like <-> instead of , then every character the forms the separator string is null-preceded:

>>> [print('{}: {}'.format(entry.food, entry.people)) for entry in entries]
hamburger: Emma<->Frank
ice cream: Donald
pizza: Adam<->Bob<->Charles 

>>> print(entries)
[('hamburger', 'Emma\x00<\x00-\x00>Frank'), ('ice cream', 'Donald'), ('pizza', 'Adam\x00<\x00-\x00>Bob\x00<\x00-\x00>Charles')]

Not sure what's going on there. But if need be, it's easy enough to strip out the nulls from the column. At least the hard part with the LISTAGG is done.

within_group can take multiple arguments. func.listagg takes what to group, followed by the separator, and within_group takes a list of what to order the group by.

query = ( select([func.listagg(A.list_value, ', ')
              .within_group(A.list_value, A.other_column)])
              .where(A.id == B.id)
              .label('list_values_of_a') )

This would translate to:

Group the list_value of A separated by a comma and a space,
and ordered by A.list_value and then A.other_column
when A.id is equal to B.id.

Hope that helps.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!