Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a LISTAGG WITHIN GROUP equivalent in SQLAlchemy?

Tags:

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.

like image 606
David White Avatar asked Feb 15 '18 02:02

David White


2 Answers

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)
like image 105
Ilja Everilä Avatar answered Sep 19 '22 12:09

Ilja Everilä


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.

like image 37
David White Avatar answered Sep 19 '22 12:09

David White