Skip to content

Add support for LISTOF function, array return, and EXPLODE operator in PyDough - #523

Open
knassre-bodo wants to merge 48 commits into
mainfrom
kian/explode
Open

Add support for LISTOF function, array return, and EXPLODE operator in PyDough#523
knassre-bodo wants to merge 48 commits into
mainfrom
kian/explode

Conversation

@knassre-bodo

@knassre-bodo knassre-bodo commented May 26, 2026

Copy link
Copy Markdown
Contributor

Linked ticket

Closes #521
Closes #522

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Docs / config

What changed and why?

Adds the LISTOF function to PyDough to aggregate data into arrays, and the EXPLODE operator to flatten such arrays into multiple rows of table data. Also added support for DataFrame collections containing arrays, which become array literals in the SQL.

  • All of these features are not supported for ANSI or SQLITE because those dialects do not have those capabilities.
  • Array literals are not supported for Snowflake because the dialect does not have that capability.
  • EXPLODE is not yet supported currently for MySQL or Oracle due to additional complications with those dialects.

How I tested this?

  • Multiple new tests on TPCH data with the name prefix array_data_ to test the LISTOF and array literal features.
  • Multiple new tests on TPCH data with the name prefix explode_ to test different variations of the EXPLODE operator, both on arrays and strings. Variations include:
    • Regular collection.EXPLODE(data)
    • Inside of a child context collection.CALCULATE(x=COUNT(EXPLODE(data)))
    • Exploding array data vs string data
    • Different cardinality/distinctness information about the exploded data
    • Exploded data being used for grouping and for window operations
    • Nested explosions (e.g. splitting an already split string with collection.EXPLODE(...).EXPLODE(...))
    • Splitting strings on an empty delimiter
    • DataFrame tables with varying data-types, empty arrays, nulls
  • Error tests for incorrect uses of the EXPLODE operator (e.g. bad arguments)

Notes for reviewers

  • A few changes to how the database connections are handled to account for difference in how the connectors return arrays.
  • Several of the tests have dialect-specific skip marks. Some of these are truly unsupported features, and some are just for now unsupported.

@knassre-bodo
knassre-bodo requested review from a team, hadia206, john-sanchez31 and juankx-bodo and removed request for a team August 17, 2026 17:26
Comment on lines -796 to -798
def user_collection(self) -> PyDoughUserGeneratedCollection:
"""The wrapped user-generated collection."""
return self._parcel[0]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a thing that should be allowed with unqualified nodes, since any method or property name you add becomes something that can no longer be used as a PyDough term name.

@john-sanchez31 john-sanchez31 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job Kian, almost there. Left some comments below :)

Comment thread documentation/dsl.md
- `name` (required): the name of the collection created from the explosion operation (similar to `PARTITION`).
- `value_name` (required): a string literal declaring the name of the new column that will be used to store the exploded data.
- `index_name` (optional): a string literal declaring the name of the new column that will be used to store the indices of the exploded data. If not provided, this column is not generated. The `index_name` is required if `is_distinct` is False. The indices are 0-indexed.
- `version` (optional, default=`"array"`): either `"array"` or `"string"`, stating whether the data to explode is an array being flattened or a string being split on a delimiter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this argument really needed? Can't we just check the type of data?

@knassre-bodo knassre-bodo Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because our typing system is not fully robust/reliable

Comment thread documentation/dsl.md
- `index_name` (optional): a string literal declaring the name of the new column that will be used to store the indices of the exploded data. If not provided, this column is not generated. The `index_name` is required if `is_distinct` is False. The indices are 0-indexed.
- `version` (optional, default=`"array"`): either `"array"` or `"string"`, stating whether the data to explode is an array being flattened or a string being split on a delimiter.
- `delimiter` (optional): a string literal indicating the delimiter that should be used to split up the string if `version="string"`. If `delimiter` is an empty string, the string will be split into individual characters.
- `filtering` (optional, default=`True`): `True` if it is possible for not every row in the original collection to be preserved in the exploded sub-collection (i.e. if one of the arrays is empty), and `False` otherwise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this one kind of confusing. Will this filter on certain conditions or will be more like filtering Empty/None values? Assuming the second one, the description could be something as follow:

Suggested change
- `filtering` (optional, default=`True`): `True` if it is possible for not every row in the original collection to be preserved in the exploded sub-collection (i.e. if one of the arrays is empty), and `False` otherwise.
`filtering_empty` (optional, default=`True`): `True` will filter all empty or None values from the original collection to the exploded sub-collection, and `False` otherwise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not what this does. It is just a boolean telling PyDough whether the operation will potentially remove rows (e.g., CAN any of the rows from the original be an empty array.

Comment thread documentation/dsl.md
Comment thread documentation/dsl.md
infinity value with `DatabaseDiatect.MYSQL` an error will be raised.

> [!IMPORTANT]
> `ArrayType` is only supported for certain dialects: Trino, Postgres, DuckDB, Databricks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about Snowflake? I saw it among the supported dialects for the EXPLODE operator

@knassre-bodo knassre-bodo Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array literals aren't a thing in Snowflake; you cannot put arrays inside VALUES


```py
# For each region, list the names of all nations inside that region
Regions.CALCULATE(region_name=name, nation_names=LISTOF(nations.name))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add how the result for each of the good example would look like?

@knassre-bodo knassre-bodo Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the regions, sure. The other one is too big.

idx_index: int | None,
lateral_alias: str,
subquery_alias: str,
) -> SQLGlotExpression:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a docstring describing this implementation? What would be the final SQL for every dialect ?

Comment thread pydough/sqlglot/transform_bindings/oracle_transform_bindings.py Outdated
inner_term = "CAST('1970-01-01' AS TIMESTAMP)"
case _:
raise ValueError(
f"Cannot support empty array of type {inner_type} in Postgres."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can we add in the documentation what exactly is supported for Postgres as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. To be clear, this is a weird edge case where Postgres struggles to handle empty array literals, and this is a workaround that only works for certain types.

Comment thread pydough/unqualified/qualification.py
if self.skip_sql:
pytest.skip(f"Skipping SQL text test for {self.test_name}")

if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this new functionality. Can you check if the skip of MySQL for infinity values, BodoSQL with to_table and DuckDb/Databricks with keywords can be skipped with this new property?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add EXPLODE operator to PyDough Add LISTOF function to aggregate data into arrays

2 participants