|
85 | 85 | has_invalid_projection_merge_functions, |
86 | 86 | ) |
87 | 87 | from snowflake.snowpark._internal.utils import ( |
88 | | - is_sql_select_statement, |
89 | 88 | ExprAliasUpdateDict, |
| 89 | + is_sql_select_statement, |
| 90 | + quote_name, |
90 | 91 | ) |
91 | 92 | import snowflake.snowpark.context as context |
92 | 93 |
|
@@ -1592,6 +1593,72 @@ def select(self, cols: List[Expression]) -> "SelectStatement": |
1592 | 1593 | ) |
1593 | 1594 | ) |
1594 | 1595 |
|
| 1596 | + # When describe reduction is on and the inner select already has resolved |
| 1597 | + # attributes, infer new.attributes for this outer select by reusing datatype and |
| 1598 | + # nullable from the subquery: (0) skip if parent column names collide, (1) index |
| 1599 | + # attributes by quote_name (Snowflake identifier rules; invalid delimited forms |
| 1600 | + # raise), (2) walk new.projection, (3) only handle plain columns or Alias(column), |
| 1601 | + # (4) resolve source via the same quote_name key lookup, (5) assign only if every |
| 1602 | + # output column was inferred (length matches projection). |
| 1603 | + if self._session.reduce_describe_query_enabled and self.attributes is not None: |
| 1604 | + parent_attributes = self.attributes |
| 1605 | + projection = new.projection |
| 1606 | + inferred_attributes: Optional[List[Attribute]] = None |
| 1607 | + # Skip: no projection to walk (do not assert; leave new.attributes unchanged). |
| 1608 | + if projection is not None: |
| 1609 | + # Skip: duplicate output names on the parent — dict/lookup would be ambiguous. |
| 1610 | + attributes_by_normalized: Dict[str, Attribute] = {} |
| 1611 | + collision = False |
| 1612 | + for attr in parent_attributes: |
| 1613 | + key = quote_name(attr.name) |
| 1614 | + existing = attributes_by_normalized.get(key) |
| 1615 | + # Skip: two parent columns map to the same quote_name key. |
| 1616 | + if existing is not None and existing is not attr: |
| 1617 | + collision = True |
| 1618 | + break |
| 1619 | + attributes_by_normalized[key] = attr |
| 1620 | + if not collision: |
| 1621 | + inferred_attributes = [] |
| 1622 | + for expr in projection: |
| 1623 | + source_column_name: Optional[str] = None |
| 1624 | + projected_column_name: Optional[str] = None |
| 1625 | + if isinstance(expr, (Attribute, UnresolvedAttribute)): |
| 1626 | + source_column_name = expr.name |
| 1627 | + projected_column_name = expr.name |
| 1628 | + elif isinstance(expr, Alias) and isinstance( |
| 1629 | + expr.child, (Attribute, UnresolvedAttribute) |
| 1630 | + ): |
| 1631 | + source_column_name = expr.child.name |
| 1632 | + projected_column_name = expr.name |
| 1633 | + else: |
| 1634 | + # Skip: not a plain column or Alias(Attribute|UnresolvedAttribute). |
| 1635 | + inferred_attributes = [] |
| 1636 | + break |
| 1637 | + |
| 1638 | + if source_column_name is None or projected_column_name is None: |
| 1639 | + # Skip: missing projected output name. |
| 1640 | + inferred_attributes = [] |
| 1641 | + break |
| 1642 | + source_attr = attributes_by_normalized.get( |
| 1643 | + quote_name(source_column_name) |
| 1644 | + ) |
| 1645 | + # Skip: no parent column for this source name. |
| 1646 | + if source_attr is None: |
| 1647 | + inferred_attributes = [] |
| 1648 | + break |
| 1649 | + inferred_attributes.append( |
| 1650 | + Attribute( |
| 1651 | + projected_column_name, |
| 1652 | + source_attr.datatype, |
| 1653 | + source_attr.nullable, |
| 1654 | + ) |
| 1655 | + ) |
| 1656 | + if len(inferred_attributes) != len(projection): |
| 1657 | + # Skip: incomplete inference (includes defensive mismatch). |
| 1658 | + inferred_attributes = None |
| 1659 | + if inferred_attributes is not None: |
| 1660 | + new.attributes = inferred_attributes |
| 1661 | + |
1595 | 1662 | new.flatten_disabled = disable_next_level_flatten |
1596 | 1663 | assert new.projection is not None |
1597 | 1664 | new._column_states = derive_column_states_from_subquery( |
|
0 commit comments