Skip to content

fix(core): know an unaliased table by its name - #15

Merged
IL-William merged 1 commit into
collinfrom
fix/unaliased-table-scope
Oct 30, 2026
Merged

IL-William merged 1 commit into
collinfrom
fix/unaliased-table-scope

Conversation

@IL-William

@IL-William IL-William commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Patch 13 of the engine fork that collin pins (dbt-collin decision 0028). A table or CTE read without an alias is now known by its bare name in its own scope.

Until now it registered nothing there, so a qualifier naming it was looked up in the enclosing scopes first:

SELECT reviews.id FROM events AS reviews
WHERE EXISTS (SELECT 1 FROM raw.reviews WHERE reviews.book_id = 1)

put the subquery's filter on events, quoted table or not; with FROM raw.reviews AS reviews it landed right. Since patch 12 (#12) a select-list subquery is analysed too, and collin's tests for correlated subqueries caught it there. Where no alias competed, the qualifier fell through to the bare name taken as a table, without its schema, so SELECT orders.id FROM db.sch.orders read no column at all.

The change

  • A TableFactor::Table without an alias, table or CTE, registers its bare name in its scope (Scope::bare_tables), normalized as qualifiers are (normalize_identifier), mapping to its canonical.
  • resolve_table_alias and resolve_alias_instance check, innermost scope first: the aliases, the bare names, then the names with no table behind them; the first scope that knows the qualifier answers.
  • A bare name two different relations of one scope share resolves to neither and stops the search; the same relation twice keeps it.
  • A LATERAL VIEW's name, and the aliases of table functions, PIVOT, UNPIVOT and UNNEST, are registered normalized in the scope (register_subquery_alias), so they shadow an outer alias of their name too. The global alias set keeps them as written, as before.

Left as it is:

  • a quoted table's bare key is folded as if unquoted, which matters only for SQL that would not compile on Postgres or Snowflake;
  • the qualifier reaches the bare-name lookup unquoted, so a quoted outer alias is folded: FROM events AS "Reviews" WHERE EXISTS (SELECT 1 FROM raw.reviews WHERE "Reviews".kind = 'x') puts the filter on reviews, with a warning that it has no kind, where master placed none; master already fails a quoted alias in the outer select;
  • an ordinary alias still matches exactly as written (DuckDB's UNNEST(...) AS U(id) under an outer u), as before this patch.

Tests

  • an unaliased table shadows an outer alias of its name, in an EXISTS and in a select-list subquery, quoted and not, on Snowflake and DuckDB;
  • an unaliased CTE shadows one too;
  • an outer unaliased table is still read through its bare name;
  • columns qualified with the bare names of unaliased tables are read;
  • a bare name two tables share resolves to neither;
  • a LATERAL VIEW name (e and E), a TABLE(GENERATOR(...)) alias and an upper-case UNNEST alias shadow an outer alias.

Each fails without its part of the patch. cargo test -p flowscope-core, cargo fmt --all -- --check and CI's cargo clippy --workspace --locked -- -D warnings pass. The same commit applies unchanged to collin and master.

Measured through collin

With a local [patch] on this branch, collin's main:

Project Effect
lineage-library book_ratings (with patch 12), loan_titles and shelf_tags give their lineage as written: nineteen marks go, nothing else moves
collin's tests the two correlated-subquery tests that failed on patch 12 alone pass; one test that relied on the engine's false alarm for a CTE named like the alias inside it now finds none
Jaffle Shop cache identical, --indirect too
Shopify cache identical, --indirect too

🤖 Generated with Claude Code

https://claude.ai/code/session_018LDZwk297SKdJXXLpTFZEh

A table read without an alias registered nothing in its scope, so a
qualifier naming it was looked up in the enclosing scopes first, and
an outer alias of the same name won:

    SELECT reviews.id FROM events AS reviews
    WHERE EXISTS (SELECT 1 FROM raw.reviews WHERE reviews.book_id = 1)

put the subquery's filter on events. Since the twelfth patch a
select-list subquery is analysed too, and did the same. Where no alias
competed, the qualifier fell through to the bare name taken as a
table, without its schema, so `SELECT orders.id FROM db.sch.orders`
read no column at all.

A table or CTE read without an alias now registers its bare name in
its scope, normalized as qualifiers are, and the alias and instance
lookups check, innermost scope first, the aliases, the bare names,
then the names with no table behind them. A bare name two different
relations of one scope share resolves to neither and stops the
search. A LATERAL VIEW's name, and the aliases of table functions,
PIVOT, UNPIVOT and UNNEST, are registered normalized in the scope, so
they shadow an outer alias of the same name too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LDZwk297SKdJXXLpTFZEh
@IL-William
IL-William force-pushed the fix/unaliased-table-scope branch from 3d0e4a7 to ac021b3 Compare October 30, 2026 21:31
@IL-William
IL-William merged commit 6870294 into collin Oct 30, 2026
@IL-William
IL-William deleted the fix/unaliased-table-scope branch October 30, 2026 21:45
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