Skip to content

Commit 2284484

Browse files
authored
Fix(optimizer): infinite recursion in the resolver (#7737)
1 parent f3ba8e4 commit 2284484

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

sqlglot/optimizer/resolver.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,8 @@ def get_source_columns(self, name: str, only_visible: bool = False) -> Sequence[
157157
):
158158
columns = source_expr.named_selects
159159

160-
# in bigquery, unnest structs are automatically scoped as tables, so you can
161-
# directly select a struct field in a query.
162-
# this handles the case where the unnest is statically defined.
160+
# in bigquery, unnest structs are automatically scoped as tables, so you can directly select
161+
# a struct field in a query. This handles the case where the unnest is statically defined.
163162
if self.dialect.UNNEST_COLUMN_ONLY and isinstance(source_expr, exp.Unnest):
164163
if not source_expr.type or source_expr.type.is_type(exp.DType.UNKNOWN):
165164
unnest_expr = seq_get(source_expr.expressions, 0)
@@ -178,7 +177,10 @@ def get_source_columns(self, name: str, only_visible: bool = False) -> Sequence[
178177
):
179178
explode_col = source_expr.this.this
180179

181-
if isinstance(explode_col, exp.Column) and source.parent:
180+
# If the column is unqualified at this point, it couldn't be resolved when
181+
# this scope's children were qualified; disambiguating it here would require
182+
# enumerating this very source's columns, i.e recurse without bound
183+
if isinstance(explode_col, exp.Column) and explode_col.table and source.parent:
182184
col_type = self._get_unnest_column_type(explode_col, source.parent)
183185
columns.extend(self._struct_field_names(col_type))
184186
elif isinstance(source, Scope) and isinstance(source.expression, exp.SetOperation):

tests/test_optimizer.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,32 @@ def test_validate_columns(self):
750750
)
751751
self.assertEqual(expression.selects[0].type, exp.DataType.build("DOUBLE", dialect="spark"))
752752

753+
# An unqualified struct field is disambiguated through the lateral's extended columns
754+
schema = {"my_table": {"items": "ARRAY<STRUCT<name STRING, age INT>>"}}
755+
self.assertEqual(
756+
optimizer.qualify.qualify(
757+
parse_one(
758+
"SELECT name FROM my_table LATERAL VIEW EXPLODE(items) ci AS ci",
759+
read="spark",
760+
),
761+
schema=schema,
762+
dialect="spark",
763+
).sql(dialect="spark"),
764+
"SELECT `ci`.`name` AS `name` FROM `my_table` AS `my_table` LATERAL VIEW EXPLODE(`my_table`.`items`) ci AS `ci`",
765+
)
766+
767+
# Resolving an unqualified lateral column whose table is missing from the schema must
768+
# raise instead of recursing infinitely
769+
with self.assertRaisesRegex(OptimizeError, "Column 'ITEMS' could not be resolved"):
770+
optimizer.qualify.qualify(
771+
parse_one(
772+
"SELECT f.value AS v FROM my_db.raw.events, LATERAL FLATTEN(items) AS f",
773+
read="snowflake",
774+
),
775+
schema={"my_db": {"other": {"some_view": {"v": "VARIANT"}}}},
776+
dialect="snowflake",
777+
)
778+
753779
def test_qualify_columns__with_invisible(self):
754780
schema = MappingSchema(self.schema, {"x": {"a"}, "y": {"b"}, "z": {"b"}})
755781
self.check_file("qualify_columns__with_invisible", qualify_columns, schema=schema)

0 commit comments

Comments
 (0)