diff --git a/sql_metadata/nested_resolver.py b/sql_metadata/nested_resolver.py index 91687c13..faf8e740 100644 --- a/sql_metadata/nested_resolver.py +++ b/sql_metadata/nested_resolver.py @@ -466,6 +466,9 @@ def _lookup_alias_in_nested( """ for nested_name in names: nested_def = definitions[nested_name] + # Empty CTE/subquery bodies cannot be re-parsed (Parser("") asserts). + if not nested_def: + continue nested_parser = parser_cache.setdefault( nested_name, self._parser_factory(nested_def) ) @@ -515,6 +518,9 @@ def _resolve_nested_query( return [subquery_alias] sub_query, column_name = parts[0], parts[-1] sub_query_definition = nested_queries[sub_query] + # Empty bodies have no columns/aliases to resolve through. + if not sub_query_definition: + return [] if column_name == "*" else [subquery_alias] subparser = already_parsed.setdefault( sub_query, self._parser_factory(sub_query_definition) ) @@ -667,7 +673,7 @@ def _cte_nodes(self) -> list[exp.CTE]: # ------------------------------------------------------------------- @staticmethod - def _body_sql(node: exp.Expression) -> str: + def _body_sql(node: exp.Expression | None) -> str: """Render an AST node to SQL, stripping identifier quoting. Example SQL:: @@ -675,7 +681,10 @@ def _body_sql(node: exp.Expression) -> str: WITH cte AS (SELECT "id" FROM "users") ... Renders the CTE body as ``SELECT id FROM users`` (quotes stripped). + Empty CTE bodies (``WITH a AS ()``) leave ``cte.this`` as None. """ + if node is None: + return "" body = node.copy() for ident in body.find_all(exp.Identifier): ident.set("quoted", False) diff --git a/test/test_with_statements.py b/test/test_with_statements.py index 67f0c0d0..d2e538a6 100644 --- a/test/test_with_statements.py +++ b/test/test_with_statements.py @@ -715,6 +715,34 @@ def test_with_queries_empty_when_no_cte(): assert p.with_queries == {} +def test_with_queries_empty_cte_body(): + """Empty CTE body WITH a AS () must not AttributeError on with_queries.""" + p = Parser("WITH a AS () SELECT 1") + assert p.with_queries == {"a": ""} + assert p.columns == [] + assert p.with_names == ["a"] + + +def test_empty_cte_body_columns_do_not_crash(): + """Empty CTE bodies must not assert via nested Parser('').""" + p = Parser("WITH a AS () SELECT * FROM a") + assert p.with_queries == {"a": ""} + assert p.columns == ["*"] + + # Sibling empty CTE must not poison star resolution of a real CTE. + p2 = Parser("WITH a AS (), b AS (SELECT 1 AS x) SELECT * FROM b") + assert p2.with_queries == {"a": "", "b": "SELECT 1 AS x"} + assert p2.columns_dict["select"] == ["*", "x"] + + # Qualified refs exercise _resolve_nested_query empty-body branch. + p_star = Parser("WITH a AS () SELECT a.* FROM a") + assert p_star.columns == [] + assert p_star.columns_dict.get("select") == [] + + p_col = Parser("WITH a AS () SELECT a.x FROM a") + assert p_col.columns == ["a.x"] + + def test_cte_subquery_full_resolution(): """Subquery + CTE: CTE-qualified columns fully resolved.""" parser = Parser("""