Here is a simple Oracle table:
+-----------+---------+
| food | person |
+-----------+---------+
| pizza | Adam |
| pizza | Bob |
| pizza | Charles |
| ice cream | Donald |
| hamburger | Emma |
| hamburger | Frank |
+-----------+---------+
And here are the results of an aggregated SELECT I'd like to do:
+-----------+------------------+
| food | people |
+-----------+------------------+
| hamburger | Emma,Frank |
| ice cream | Donald |
| pizza | Adam,Bob,Charles |
+-----------+------------------+
With Oracle 11g+ this is easy enough with a LISTAGG:
SELECT food, LISTAGG (person, ',') WITHIN GROUP (ORDER BY person) AS people
FROM mytable
GROUP BY food;
But I haven't been able to find a way to do this within SQLAlchemy. An old question from Stack Overflow shows where someone was trying to implement a custom class to do the job, but is that really the best option there is?
MySQL has a group_concat
feature, and thus this questioner solved his problem with func.group_concat(...)
. Sadly that function is not available within Oracle.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With