I've stumbled upon the same issue and came up with this (probably incomplete and not very thoroughly tested) implementation. It's also based on ideas from sqlalchemy-utils but avoids hard psycopg2 dependency.
Mind that there is no CompositeArray
equivalent. It seems that no custom type is actually required for such arrays. The same functionality can be achieved by explicitly declaring dimensions and item type of your ARRAY
of composites and SQLAlchemy will properly pass each array entry to result_processor
of item's UserDefinedType
(and hence to process_result_value
of TypeDecorator
wrapper).
All in all, the following code demonstrates intended usage (tested on Python 3.11 and SQLAlchemy 1.4):
import asyncio
import dataclasses
import pprint
from contextlib import AsyncExitStack
from decimal import Decimal
import sqlalchemy as sa
import sqlalchemy.ext.asyncio
from sqlalchemy.dialects import postgresql as sa_pg
import composite
if __name__ == "__main__":
async def main():
metadata = sa.MetaData()
demo_composite_type = composite.define_composite(
"demo_composite",
metadata,
("field1", sa.TEXT),
("field2", sa.Numeric),
)
# NOTE: The following class might be omitted if `asyncpg.Record` as a return type is sufficient for your
# needs and there is no need to convert items to some custom (data)class.
class demo_composite_type(sa.types.TypeDecorator):
impl, cache_ok = demo_composite_type, True
python_type = dataclasses.make_dataclass(
"DemoComposite",
[name for name in demo_composite_type.fields],
)
def process_result_value(self, value, dialect):
if value is not None:
return self.python_type(**value)
async with AsyncExitStack() as deffer:
pg_engine = sa.ext.asyncio.create_async_engine(
"postgresql+asyncpg://scott:tiger@localhost/test"
)
deffer.push_async_callback(pg_engine.dispose)
pg_conn = await deffer.enter_async_context(
pg_engine.connect()
)
await pg_conn.run_sync(metadata.create_all)
deffer.push_async_callback(pg_conn.run_sync, metadata.drop_all)
values_stmt = (
sa.sql.Values(
sa.column("column1", sa.TEXT),
sa.column("column2", sa.Numeric),
)
.data([
("1", Decimal(1)),
("2", Decimal(2)),
])
.alias("samples")
)
result = (await pg_conn.execute(
sa.select([
sa.func.array_agg(
sa.cast(
sa.tuple_(
values_stmt.c.column1,
values_stmt.c.column2,
),
demo_composite_type,
),
type_=sa_pg.ARRAY(
demo_composite_type,
dimensions=1,
),
)
.label("array_of_composites")
])
.select_from(
values_stmt
)
)).scalar_one()
pprint.pprint(result)
asyncio.run(main())
Output of the sample above is the following:
[DemoComposite(field1='1', field2=Decimal('1')),
DemoComposite(field1='2', field2=Decimal('2'))]