From fdefa1289b2ff6d4fce126e09f3b3d3803f2c3b5 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Mon, 30 Mar 2026 13:46:47 +0800 Subject: [PATCH] fix(wren): case-insensitive CTE name shadowing in CTERewriter _collect_user_cte_names() collected raw CTE names (e.g. "Orders") while _collect_model_columns() compared against normalized/lowercased table names ("orders"). The case-sensitive check missed the shadow, generating a duplicate CTE. Lowercase names on collection to match sqlglot's normalize_identifiers behavior. Closes #1479 (comment) Co-Authored-By: Claude Opus 4.6 (1M context) --- wren/src/wren/mdl/cte_rewriter.py | 3 ++- wren/tests/unit/test_cte_rewriter.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/wren/src/wren/mdl/cte_rewriter.py b/wren/src/wren/mdl/cte_rewriter.py index d4a59fdfc..fe373687f 100644 --- a/wren/src/wren/mdl/cte_rewriter.py +++ b/wren/src/wren/mdl/cte_rewriter.py @@ -226,9 +226,10 @@ def _collect_user_cte_names(ast: exp.Expression) -> set[str]: for cte in with_clause.expressions: alias = cte.args.get("alias") if alias: - names.add( + raw = ( alias.this.name if isinstance(alias.this, exp.Identifier) else str(alias.this) ) + names.add(raw.lower()) return names diff --git a/wren/tests/unit/test_cte_rewriter.py b/wren/tests/unit/test_cte_rewriter.py index aec5d122e..7395ef90c 100644 --- a/wren/tests/unit/test_cte_rewriter.py +++ b/wren/tests/unit/test_cte_rewriter.py @@ -208,6 +208,22 @@ def test_user_cte_shadows_model(self): # (wren-core may restructure the query but won't add a model CTE) assert not _has_cte(result, "orders") or _count_ctes(result) <= 1 + def test_user_cte_shadows_model_case_insensitive(self): + """User CTE 'Orders' (mixed case) should shadow model 'orders'.""" + rw = _make_rewriter(_SINGLE_MODEL_MANIFEST, fallback=True) + result = rw.rewrite( + "WITH Orders AS (SELECT 1 AS o_orderkey, 'OPEN' AS o_orderstatus) " + "SELECT * FROM Orders" + ) + ast = sqlglot.parse_one(result, dialect="duckdb") + with_clause = ast.args.get("with_") + if with_clause: + cte_names = [ + cte.args["alias"].this.name for cte in with_clause.expressions + ] + orders_count = sum(1 for n in cte_names if n.lower() == "orders") + assert orders_count <= 1, f"Duplicate 'orders' CTE: {result}" + def test_nested_cte_shadows_model(self): """CTE defined in a subquery WITH should also shadow the model name.""" rw = _make_rewriter(_MULTI_MODEL_MANIFEST, fallback=True)