Skip to content

Give each CTE its own node when a nested WITH reuses its name - #5

Merged
IL-William merged 1 commit into
masterfrom
fix/cte-scopes-master
Oct 10, 2026
Merged

IL-William merged 1 commit into
masterfrom
fix/cte-scopes-master

Conversation

@IL-William

Copy link
Copy Markdown
Owner

Patch 7 of the engine fork that collin pins (dbt-collin decision 0028): two CTEs of one name in nested WITH blocks get a node and a column list of their own, instead of sharing the first one's. dbt writes this whenever it inlines ephemeral models that each name a CTE final, orders or ranked.

Without the patch, every reader of either CTE gets both lineages, and the engine raises no issue. collin works around it by setting such compiles aside (dbt-collin decision 0018). This patch lets it read them.

The change

  • The key. A CTE is keyed name#n from its second occurrence in a statement, as patch 4 already does for derived tables. The first occurrence keeps its key, so a statement that never repeats a name is analysed as before.
  • The scope. A WITH that redefines a name hides the earlier definition and its columns until its query ends, then puts them back.
  • Inference through SELECT * without a schema. Wildcards are now keyed by node, not by name. This also covers derived tables that reuse an alias.

There are 8 new tests. 6 fail without the patch; 2 pass either way and serve as guards. 2620 tests pass. cargo fmt --check is clean.

Measured through collin

Each project was run with and without --indirect.

Project Effect
Fivetran's Shopify package, 240 models on DuckDB the 630 inferred edges of 16 marts become 1303 parsed edges, and 5547 of the 5548 parsed edges match sqlglot's reading
Cal-ITP, 624 BigQuery models 579 more parsed edges and 500 fewer inferred
A 3507 model Snowflake project cache identical

Nothing goes to pondpilot/flowscope.

🤖 Generated with Claude Code

https://claude.ai/code/session_017VTEw2dNNL9Pn57Hkeeirn

A CTE's node was keyed by statement and name, and its output columns were
kept in one map per statement keyed by name. Two CTEs of one statement
sharing a name in different WITH blocks, as dbt writes when it inlines
ephemeral models that each name a CTE `final`, therefore shared one node
and one column list, and each reader received both lineages:

    WITH a AS (
        WITH final AS (SELECT k, ts FROM sat_a) SELECT k, ts FROM final
    ),
    b AS (
        WITH final AS (SELECT k, ts FROM sat_b) SELECT k, ts FROM final
    )
    SELECT a.ts AS a_ts, b.ts AS b_ts FROM a JOIN b ON a.k = b.k

a_ts came out derived from sat_a.ts and sat_b.ts both, and so did b_ts,
with no issue raised.

Count the CTEs of each name per statement, as derived tables already are.
The first keeps the key it always had, so nothing changes for a statement
that does not repeat a name, and each later one is keyed `name#n`, with a
node and column nodes of its own and the name as its label. A WITH that
defines a name an earlier CTE has hides that CTE's definition and columns
until its query ends, then puts them back, so a name read after a nested
WITH is the one in scope there.

Backward inference through `SELECT *` grouped wildcards by the target's
name and chained them by the source's. It now keys them by node, so two
CTEs of one name with no schema infer their own columns; a derived table
reusing an alias, which the earlier fix left to inference by name, is
covered too.

Tests: the query above; the same inside two derived tables; an outer CTE
shadowed in a nested WITH and read after it; the same with no schema; a
filter on one of two CTEs of one name; an unqualified column of an outer
CTE beside an inner namesake. All six fail without the change. Two more
pass either way and guard it: stars expanded per CTE, and dbt's shape, an
outer `final` over an inlined model holding its own `final`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IL-William
IL-William merged commit 7218b39 into master Oct 10, 2026
8 checks passed
@IL-William
IL-William deleted the fix/cte-scopes-master branch October 10, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant