Skip to content

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

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

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

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-master branch from ed2d0cd to 34116bb Compare October 30, 2026 21:31
@IL-William
IL-William merged commit b904397 into master Oct 30, 2026
8 checks passed
@IL-William
IL-William deleted the fix/unaliased-table-scope-master branch October 30, 2026 21:44
IL-William added a commit to IL-William/dbt-collin that referenced this pull request Oct 30, 2026
It drops the nineteen marks the engine side of two Snowflake shapes left
after #48: `book_ratings` (a scalar subquery in a select list),
`loan_titles` and `shelf_tags` (a table without an alias whose columns
are qualified with its own name).

**The pin.** `rev` moves to `b904397`, tagged `collin-engine-8`: the
fork's `master` with

- **patch 12** (IL-William/flowscope#12, #13): a scalar subquery in a
  select list is analysed against a node of its own, and its column
  feeds the item. It was never analysed, so its column was drawn from
  the `FROM` table with the subquery's text, and `engine.rs` read the
  outer side of a correlation out of that text as the value;
- **patch 13** (IL-William/flowscope#14, #15): a table or CTE read
  without an alias is known by its bare name in its scope. With patch 12
  alone (`collin-engine-7`, never pinned), two tests of 0049 failed: the
  engine now placed a subquery's filter, and an unaliased inner table
  did not shadow an outer alias of its name. It also reads a column
  qualified with an unaliased table's bare name, which fell to a name
  without its schema.

**What moves.**

| | Before | After |
| --- | --- | --- |
| lineage-library facts given | 192 of 221 | 203 of 221 |
| Snowflake shapes marked | 5 | 3: `TABLE(FLATTEN(...))`, a star inside a call, a lambda's argument |
| wrong lines given | 6 | 5 |
| Jaffle Shop cache and totals, with and without `--indirect` | | identical |
| Shopify cache and totals, with and without `--indirect` | | identical |

One test,
`a_column_a_child_reads_and_its_parent_s_list_lacks_is_the_parent_s_finding`,
held a CTE named like the alias inside it, where the engine raised a
false alarm that stayed on the child; the engine now reads it right and
the child has no entry, so the case asserts that. The rule it covered, a
column the parent lacks with no edge behind the child's read, which then
stays on the child, has a new case: `quiet_kid` reads `amount` only in a
`WHERE`; it fails with the rule removed.

**Docs.** 0028 lists the eleventh to thirteenth patches and records the
move; `docs/next-steps.md` item 2 and the README's known limits name the
three shapes left, and item 3 counts thirteen patches. No version bump.

246 tests pass with `cargo test --release`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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