Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3420f45
Start of explode tests, need to fill out rest
knassre-bodo May 13, 2026
a9a5c6c
Adding LISTOF operator WIP
knassre-bodo May 15, 2026
c7ec035
Fixed snowflake array data returning via json.loads
knassre-bodo May 22, 2026
e61265b
Merge branch 'main' into kian/explode
knassre-bodo May 26, 2026
baf47cb
WIP
knassre-bodo May 27, 2026
91a18cc
Experimenting with ordering
knassre-bodo May 29, 2026
766f2ea
Merge branch 'main' into kian/explode
knassre-bodo Jun 1, 2026
6a1d179
Fixing array_agg testing for dialects
knassre-bodo Jun 2, 2026
6c89f4e
WIP array literal generation on dialects
knassre-bodo Jun 2, 2026
4ecdaf4
Array literal dialect WIP
knassre-bodo Jun 2, 2026
f6f0877
Array data tests handled except for ORACLE
knassre-bodo Jun 4, 2026
99098a0
Testing WIP
knassre-bodo Jun 11, 2026
4120c9b
Test run [RUN ORACLE]
knassre-bodo Jun 11, 2026
df06954
Test run oracle 2 [RUN ORACLE]
knassre-bodo Jun 11, 2026
3be7bd3
Early testing and unqualified nodes WIP
knassre-bodo Jun 17, 2026
d2057e4
Resolving conflicts
knassre-bodo Jun 22, 2026
3f6fb6f
WIP hybrid/relational translation of EXPLODE operator
knassre-bodo Jul 23, 2026
2bdf031
Switched to ExplodeSpec handling of misc args
knassre-bodo Jul 24, 2026
d0473e5
WIP Relational steps
knassre-bodo Jul 27, 2026
ac158cb
Continued work on relational handling
knassre-bodo Jul 29, 2026
e72bb2f
WIP relational handling and qdag collection access
knassre-bodo Jul 31, 2026
10ac9d9
Skipped accessing sub-collections, for now
knassre-bodo Aug 3, 2026
df4308e
Minor testing patch
knassre-bodo Aug 5, 2026
c727d6b
Three explode tests working in Snowflake
knassre-bodo Aug 6, 2026
ac53caf
Adding POSTGRES support
knassre-bodo Aug 6, 2026
799534d
Added generated alias, fixed some of problems with multi-explode
knassre-bodo Aug 7, 2026
cea2df7
Added more tests
knassre-bodo Aug 7, 2026
2462791
Added more tests, fixed filter pushdown bugs
knassre-bodo Aug 7, 2026
314c7d4
Added databricks support
knassre-bodo Aug 11, 2026
9edcbf2
Merging conflcits, adding DuckDB support
knassre-bodo Aug 11, 2026
f375231
Added duckdb explode support
knassre-bodo Aug 12, 2026
24de864
Adding trino support
knassre-bodo Aug 14, 2026
4a077e5
Added more tests, need to fix lateral ON condition [RUN DIALECTS]
knassre-bodo Aug 14, 2026
4910fc5
Fixing LATERAL ON bug
knassre-bodo Aug 17, 2026
dafd2c1
Added documentation, fixed bugs with name collisions by qualifying un…
knassre-bodo Aug 18, 2026
84037af
Merge branch 'main' into kian/explode
knassre-bodo Aug 20, 2026
51517f6
[RUN CI][RUN DIALECTS]
knassre-bodo Aug 20, 2026
069ad2e
Fixing test bugs [RUN CI][RUN DIALECTS]
knassre-bodo Aug 20, 2026
8c57b74
Merge branch 'main' into kian/explode
knassre-bodo Aug 20, 2026
ad51c98
Revisions
knassre-bodo Aug 20, 2026
ce0979d
Adding explode literal array test
knassre-bodo Aug 20, 2026
2d60647
Added exploding string literal test
knassre-bodo Aug 20, 2026
4ff2736
Revisions, testing fixes [RUN CI][RUN DIALECTS]
knassre-bodo Aug 21, 2026
2adbb90
Fixed bugs, added docs, test run to epxpose trino issue [RUN TRINO]
knassre-bodo Aug 21, 2026
c8f706b
Fixing trino bug, adding BodoSQL test [RUN CI][RUN DIALECTS]
knassre-bodo Aug 21, 2026
f6b2431
Explode test fix [RUN CI][RUN DIALECTS]
knassre-bodo Aug 28, 2026
c80670c
Explode test fix [RUN CI][RUN DIALECTS]
knassre-bodo Aug 28, 2026
4871bf9
Explode test fix [RUN CI][RUN DIALECTS]
knassre-bodo Aug 28, 2026
af9b38c
Revisions [RUN CI][RUN DIALECTS]
knassre-bodo Sep 9, 2026
c7f9979
Regular test fixes [RUN CI]
knassre-bodo Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 149 additions & 2 deletions documentation/dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This page describes the specification of the PyDough DSL. The specification incl
* [SINGULAR](#singular)
* [BEST](#best)
* [CROSS](#cross)
* [EXPLODE](#explode)
- [User Generated Collections](#user-generated-collections)
* [range_collection](#range_collection)
* [dataframe_collection](#dataframe_collection)
Expand All @@ -36,7 +37,7 @@ This page describes the specification of the PyDough DSL. The specification incl
## Example Graph

The examples in this document use a metadata graph (named `GRAPH`) with the following collections:
- `People`: records of every known person. Scalar properties: `first_name`, `middle_name`, `last_name`, `ssn`, `birth_date`, `email`, `current_address_id`.
- `People`: records of every known person. Scalar properties: `first_name`, `middle_name`, `last_name`, `ssn`, `birth_date`, `email`, `current_address_id`, `phone_numbers`.
- `Addresses`: records of every known address. Scalar properties: `address_id`, `street_number`, `street_name`, `apartment`, `zip_code`, `city`, `state`.
- `Packages`: records of every known package. Scalar properties: `package_id`, `customer_ssn`, `shipping_address_id`, `billing_address_id`, `order_date`, `arrival_date`, `package_cost`.

Expand Down Expand Up @@ -1536,6 +1537,147 @@ People.CALCULATE(Packages=COUNT(People.packages)).CROSS(Packages)
People.CROSS(Addresses).current_address
```

<!-- TOC --><a name="explode"></a>
### EXPLODE

A PyDough operation that explodes each row from a collection into multiple rows, i.e. from flattening a column of array data, or by splitting up a string column on a delimiter. The outputted collection will be a sub-collection of the original context containing the exploded data, and an optional indexed column keeping track of the indices of each value of the exploded data within a single row. This newly generated sub-collection has no other sub-collections from the original collection. The syntax for this operation is `collection.EXPLODE(...)`. `EXPLODE` has the following arguments:
- `data` (required): the expression from the current context being exploded (either an array or string). This expression cannot reference any sub-collections of the current context.
- `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

- `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.

- `is_distinct` (optional, default=`False`): `True` if each row of the exploded data is unique within the set of all other values from that same original row of unexploded data, and `False` otherwise. An `index_name` can only be omitted if `is_distinct` is `True`.

> [!IMPORTANT]
> This feature is only supported in certain dialects. It is currently supported for Snowflake, DataBricks, DuckDB, Postgres and Trino.

**Good Example #1**: List out every phone number had by every person (assuming `phone_numbers` is an array of strings).
Comment thread
knassre-bodo marked this conversation as resolved.

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number', is_distinct=True)
```

**Good Example #2**: For each person, find the first phone number they have (assuming `phone_numbers` is an array of strings).

```py
%%pydough
People.CALCULATE(first_name, last_name)
.EXPLODE(phone_numbers, "numbers", value_name='phone_number', index_name='idx', is_distinct=True)
.WHERE(idx == 0)
.CALCULATE(first_name, last_name, phone_number)
```

**Good Example #3**: For each person, count how many phone numbers they have (assuming `phone_numbers` is an array of strings).
```py
%%pydough
exploded_numbers = EXPLODE(phone_numbers, "numbers", value_name='phone_number', is_distinct=True)
People.CALCULATE(first_name, last_name, n_phone_numbers=COUNT(exploded_numbers))
```

**Good Example #4**: List out every phone number had by every person (assuming `phone_numbers` is a string of comma separated values).

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number', version="string", delimiter=",", is_distinct=True)
```

**Good Example #5**: List the number of times each character of the alphabet used within first names of people.

```py
%%pydough
People.EXPLODE(LOWER(first_name), "characters", value_name='char', index_name="idx", version="string", delimiter="")
.PARTITION(name='letters', by=char)
.CALCULATE(char, n_uses=COUNT(characters))
```

**Data Example**:

Suppose we have the following collection of data `thesaurus` containing words and some of their synonyms in an array:
| word | synonyms |
|---------|--------------------------------|
| 'wise' | ['sage', 'insightful', 'keen'] |
| 'old' | ['elderly', 'ancient'] |
| 'my' | [] |
| 'large' | ['big'] |

Now suppose the following PyDough code is used to transform `thesaurus` using the `EXPLODE` operator (note: `filtering=True` because one of the rows is an empty array):
```py
%%pydough
thesaurus.CALCULATE(word)
.EXPLODE(synonyms, "words", value_name='synonym', index_name='syn_idx', filtering=True)
.CALCULATE(word, syn_idx, synonym)
```

The result would be the following table:
| word | syn_idx | synonym |
|---------|---------|--------------|
| 'wise' | 0 | 'sage' |
| 'wise' | 1 | 'insightful' |
| 'wise' | 2 | 'keen' |
| 'old' | 0 | 'elderly' |
| 'old' | 1 | 'ancient' |
| 'large' | 0 | 'big' |

**Bad Example #1**: Missing the `name`.

```py
%%pydough
People.EXPLODE(phone_numbers, value_name='phone_number', index_name="idx")
```

**Bad Example #2**: Missing the `value_name`.

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", index_name="idx")
```

**Bad Example #3**: Missing the `index_name` when `is_distinct=True` is not provided.

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number')
```

**Bad Example #4**: Providing an invalid `version`

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number', index_name="idx", version="party")
```

**Bad Example #5**: Missing the `delimiter` when `version="string"`

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number', index_name="idx", version="string")
```

**Bad Example #6**: Not providing a string literal for the delimiter.

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number', index_name="idx", version="string", delimiter=first_name)
```

**Bad Example #7**: Attempting to access a sub-collection after exploding.

```py
%%pydough
People.EXPLODE(phone_numbers, "numbers", value_name='phone_number', index_name="idx")
.packages
```

**Bad Example #8**: Accessing a sub-collection when declaring the data to explode.

```py
%%pydough
People.EXPLODE(LISTOF(packages.package_cost), "costs", value_name='cost', index_name="idx")
```

<!-- TOC --><a name="user-generated-collections"></a>
## User Generated Collections

Expand Down Expand Up @@ -1605,12 +1747,17 @@ The supported PyDough types for `dataframe_collection` are:
- `NumericType`: includes float, integer, infinity, Nan.
- `BooleanType`: True or False.
- `StringType`: alphanumeric characters.
- `Datetype`: date and datetime.
- `DateType`: date and datetime.
- `ArrayType`: arrays of data.
- `UnknownType`: used for all `None` columns.

Note: MySQL by default does not support infinity values. When PyDough detects
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

> Postgres has a limited ability to support rows with an empty array, depending on the type of the column. These sorts of array literals are only supported when the overall column type is an array of booleans, numbers, strings, or datetime values.

#### Example 1

```python
Expand Down
29 changes: 29 additions & 0 deletions documentation/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Below is the list of every function/operator currently supported in PyDough as a
* [HASNOT](#hasnot)
* [VAR](#var)
* [STD](#std)
* [LISTOF](#listof)
- [Window Functions](#window-functions)
* [RANKING](#ranking)
* [PERCENTILE](#percentile)
Expand Down Expand Up @@ -1089,6 +1090,34 @@ Parts.CALCULATE(std = STD(supply_records.supply_cost))
Parts.CALCULATE(std = STD(supply_records.supply_cost, type="sample"))
```

<!-- TOC --><a name="std"></a>
Comment thread
knassre-bodo marked this conversation as resolved.

### LISTOF

The `LISTOF` function collects a set of values into an array.

> [!IMPORTANT]
> This function is only supported in certain dialects. It is currently supported for Snowflake, DataBricks, DuckDB, Postgres and Trino.

```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.


# For each customer, list the three largest quantities purchased made by that
# customer.
selected_lines = orders.lines.best(by=quantity.DESC(), per='Customers', n_best=3)
Customers.CALCULATE(customer_name=name, quantities=LISTOF(selected_lines.quantity))
```

The first of these examples would produce the following output:
| region_name | nation_names |
|---------------|---------------------------------------------------------------|
| "AFRICA" | ["ALGERIA", "ETHIOPIA", "KENYA", "MOROCCO", "MOZAMBIQUE"] |
| "AMERICA" | ["ARGENTINA", "BRAZIL", "CANADA", "PERU", "UNITED STATES"] |
| "ASIA" | ["INDIA", "INDONESIA", "JAPAN", "CHINA", "VIETNAM"] |
| "EUROPE" | ["FRANCE", "GERMANY", "ROMANIA", "RUSSIA", "UNITED KINGDOM"] |
| "MIDDLE EAST" | ["EGYPT", "IRAN", "IRAQ", "JORDAN", "SAUDI ARABIA"] |

<!-- TOC --><a name="window-functions"></a>

## Window Functions
Expand Down
18 changes: 15 additions & 3 deletions documentation/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,12 @@ Below are several examples the JSON for such verifiers:

The JSON for a function deducer, used in the `output signature` field of a function definition, specifies the rules for determining the output type of a call to the function in terms of its input arguments. If a verifier is not provided, the default assumption is that the function call outputs an expression of type `"unknown"`.

Each deducer has a mandatory string field `type` specifying what kind of verifier it is. The currently supported values are `"constant"` and `"select argument"`.
Each deducer has a mandatory string field `type` specifying what kind of verifier it is. The currently supported values are `"constant"`, `"select argument"` and `"array of"`.

<!-- TOC --><a name="function-deducer-type-constant"></a>
### Function Deducer Type: Constant

Function deducers of this type have a type string of `"constant"` and correspond to a function call that always returns the same type. Verifiers of this type have the following additional key-value pairs in their metadata JSON object:
Function deducers of this type have a type string of `"constant"` and correspond to a function call that always returns the same type. Deducers of this type have the following additional key-value pairs in their metadata JSON object:

- `value` (required): a type string ([see here for more information](#pydough-type-strings)) indicating what type the function always returns.

Expand All @@ -484,7 +484,7 @@ Below are several examples the JSON for such deducers:
<!-- TOC --><a name="function-deducer-type-select-argument"></a>
### Function Deducer Type: Select Argument

Function deducers of this type have a type string of `"select argument"` and correspond to a function call that always returns a value of the same type as a specific argument. Verifiers of this type have the following additional key-value pairs in their metadata JSON object:
Function deducers of this type have a type string of `"select argument"` and correspond to a function call that always returns a value of the same type as a specific argument. Deducers of this type have the following additional key-value pairs in their metadata JSON object:

- `value` (required): a non-negative integer indicating which input argument to the function call should determine the output type of the function when called.

Expand All @@ -493,6 +493,18 @@ Below are several examples the JSON for such deducers:
- Returns the type of the first argument: `{"type": "select argument", "value": 0}`
- Returns the type of the second argument: `{"type": "select argument", "value": 1}`

<!-- TOC --><a name="function-deducer-type-array-of"></a>
### Function Deducer Type: Array Of

Function deducers of this type have a type string of `"array of"` and correspond to a function call that returns an array type. Deducers of this type have the following additional key-value pairs in their metadata JSON object:

- `element type` (required): a JSON object containing the specification for another function deducer containing the element type of the array.

Below are several examples the JSON for such deducers:

- Returns an array of strings: `{"type": "array of", "element type": {"type": "constant", "value": "string"}}`
- Returns an array where the elements have the same type as the first argument: `{"type": "array of", "element type": {"type": "select argument", "value": 0}}`

<!-- TOC --><a name="pydough-type-strings"></a>
## PyDough Type Strings

Expand Down
35 changes: 35 additions & 0 deletions pydough/conversion/agg_removal.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Aggregate,
CallExpression,
EmptySingleton,
Explode,
Filter,
GeneratedTable,
Join,
Expand Down Expand Up @@ -218,6 +219,36 @@ def deduce_join_uniqueness(
return result


def deduce_explode_uniqueness(
unique_terms: set[frozenset[str]], explode: Explode
) -> set[frozenset[str]]:
"""
Helper function to transforms the uniqueness sets after an Explode
operation duplicates rows.
Comment thread
knassre-bodo marked this conversation as resolved.

Args:
`unique_terms`: the uniqueness sets of the input to the explode.
`explode`: the explode node.

Returns:
The uniqueness sets of the output of the explode.
"""
# Build up the list of column names that imply uniqueness within one of the
# expanded row sets (either the index column, or the expanded data, or
# both)
cross_terms: list[str] = []
if explode.explode_spec.index_name is not None:
cross_terms.append(explode.explode_spec.index_name)
if explode.explode_spec.is_distinct:
cross_terms.append(explode.explode_spec.value_name)
# Add each of the cross terms to all of the uniqueness sets
result: set[frozenset[str]] = set()
for term_set in unique_terms:
for cross_term in cross_terms:
result.add(term_set | frozenset([cross_term]))
return result


def aggregation_uniqueness_helper(
node: RelationalNode,
) -> tuple[RelationalNode, set[frozenset[str]]]:
Expand Down Expand Up @@ -286,6 +317,10 @@ def aggregation_uniqueness_helper(
node, unique_sets
)
return node, final_uniqueness
case Explode():
node._input, input_uniqueness = aggregation_uniqueness_helper(node.input)
input_uniqueness = bubble_uniqueness(input_uniqueness, node.columns, None)
return node, deduce_explode_uniqueness(input_uniqueness, node)
# Empty singletons don't have uniqueness information.
case EmptySingleton():
return node, set()
Expand Down
22 changes: 22 additions & 0 deletions pydough/conversion/filter_pushdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CallExpression,
ColumnReference,
EmptySingleton,
Explode,
Filter,
GeneratedTable,
Join,
Expand Down Expand Up @@ -319,6 +320,27 @@ def visit_generated_table(self, generated_table: GeneratedTable) -> RelationalNo
# cannot be pushed down any further.
return self.flush_remaining_filters(generated_table, self.filters, set())

def visit_explode(self, explode: Explode) -> RelationalNode:
pushable_filters: set[RelationalExpression]
remaining_filters: set[RelationalExpression]
# Push all filters that only depend on columns that pass through
# from the input, as opposed to being generated outputs.
allowed_cols: set[str] = set()
for name, expr in explode.columns.items():
if not (
isinstance(expr, ColumnReference)
and expr.name
in (explode.explode_spec.value_name, explode.explode_spec.index_name)
):
allowed_cols.add(name)
pushable_filters, remaining_filters = partition_expressions(
self.filters,
lambda expr: only_references_columns(expr, allowed_cols),
)
return self.flush_remaining_filters(
explode, remaining_filters, pushable_filters
)


def push_filters(node: RelationalNode, session: PyDoughSession) -> RelationalNode:
"""
Expand Down
13 changes: 13 additions & 0 deletions pydough/conversion/hybrid_decorrelater.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .hybrid_operations import (
HybridCalculate,
HybridChildPullUp,
HybridExplode,
HybridFilter,
HybridNoop,
HybridPartition,
Expand Down Expand Up @@ -307,6 +308,14 @@ def correl_ref_purge(
correl_level,
new_parent_uni_keys,
)
if isinstance(operation, HybridExplode):
operation.explode_data = self.remove_correl_refs(
operation.explode_data,
old_parent,
child_height,
correl_level,
new_parent_uni_keys,
)
# Repeat the process on the ancestor until either loop guard
# condition is no longer True. Only update the child height if we
# are still making steps from the original tree, as opposed to from
Expand Down Expand Up @@ -554,6 +563,10 @@ def find_correlated_children(self, hybrid: HybridTree) -> None:
correl_levels = max(
correl_levels, operation.condition.count_correlated_levels()
)
if isinstance(operation, HybridExplode):
correl_levels = max(
correl_levels, operation.explode_data.count_correlated_levels()
)

assert correl_levels <= len(self.stack)
for i in range(-1, -correl_levels - 1, -1):
Expand Down
Loading