Skip to content

fix(compiler): scope-aware column resolution in subqueries (#4251)#4457

Open
luongs3 wants to merge 1 commit into
sqlc-dev:mainfrom
luongs3:fix/4251-subquery-scope
Open

fix(compiler): scope-aware column resolution in subqueries (#4251)#4457
luongs3 wants to merge 1 commit into
sqlc-dev:mainfrom
luongs3:fix/4251-subquery-scope

Conversation

@luongs3
Copy link
Copy Markdown

@luongs3 luongs3 commented May 28, 2026

Fixes #4251.

Problem

When resolving an unqualified column reference inside a subquery, sqlc's analyzer matched against every table in scope across the whole query, producing a spurious column reference "id" is ambiguous error for queries that real PostgreSQL resolves unambiguously via lexical scope.

The exact repro from the issue:

CREATE TABLE t1 ( id UUID PRIMARY KEY );
CREATE TABLE t2 ( id UUID, t1_id UUID REFERENCES t1(id) ON DELETE CASCADE );

-- name: GetT1 :many
SELECT * FROM t1
WHERE id IN (
    SELECT t1_id FROM t2 WHERE id = $1
);

Real Postgres binds the outer id to t1.id and the inner id to t2.id by lexical scope. sqlc was flattening every FROM-clause RangeVar in the whole query into one search list and triggering "ambiguous" on the inner id.

Fix

Two small changes:

  • internal/compiler/find_params.go — when paramSearch.Visit enters a SelectStmt whose FROM clause has exactly one RangeVar, capture it as the current scope. The walker propagates this through the returned visitor, so ParamRefs in that SelectStmt's WHERE/GROUP inherit the inner scope.

    The exactly-one guard ensures we don't silently pick a winner when the FROM is genuinely multi-table at the same level — true ambiguity (e.g. SELECT t1.id FROM t1, t2 WHERE id = $1) must still error.

  • internal/compiler/resolve.go — when resolving an unqualified column, if no alias is given but ref.rv points to an in-scope table that actually contains the column, narrow the search to that table only. If the column is absent from the inner table, fall back to the full search list so correlated-subquery references to an outer column continue to work.

Tests

  • internal/compiler/resolve_test.go — new unit test covering narrowToInnermostScope across nil-rv, column-in-inner-scope (narrow), column-absent (fall back), and unknown-table (fall back).
  • internal/endtoend/testdata/subquery_scope_4251/ — end-to-end fixture reproducing the exact query under postgresql/pgx/v5; the generated code is asserted against golden files.

Verified locally

  • The repro now generates cleanly (EXIT=0, was EXIT=1).
  • SELECT t1.id FROM t1, t2 WHERE id = $1 still errors with "ambiguous" — no regression.
  • Correlated subquery (inner WHERE referring to an outer column) still resolves cleanly.
  • go test ./internal/compiler/... ./internal/sql/... ./internal/engine/... passes; gofmt -l and go vet clean on the touched files.

…4251)

When resolving an unqualified column reference inside a subquery, sqlc's
analyzer matched against every table in scope across the whole query,
producing a spurious "column reference ... is ambiguous" error for
queries that real PostgreSQL resolves unambiguously via lexical scope.

Example from the issue:

    SELECT * FROM t1 WHERE id IN (
        SELECT t1_id FROM t2 WHERE id = $1
    );

Real Postgres binds the outer "id" to t1.id and the inner "id" to
t2.id by lexical scope. sqlc was flattening every FROM-clause RangeVar
in the whole query into one search list and triggering "ambiguous" on
the inner id.

Two small changes:

* internal/compiler/find_params.go — when paramSearch.Visit enters a
  SelectStmt whose FROM clause has exactly one RangeVar, capture it as
  the current scope (paramSearch.rangeVar). The walker propagates this
  through the returned visitor, so ParamRefs encountered in the same
  SelectStmt's WHERE/GROUP/etc. inherit the inner scope. The
  exactly-one guard ensures we don't silently pick a winner when the
  FROM clause is genuinely multi-table at the same level — those cases
  must keep iterating every table so true ambiguity (e.g.
  "SELECT t1.id FROM t1, t2 WHERE id = $1") still errors.

* internal/compiler/resolve.go — when resolving an unqualified column,
  if no alias is given but ref.rv points to an in-scope table that
  actually contains the column, narrow the search to that table only.
  If the column is absent from the inner table, fall back to the full
  search list so correlated-subquery references to an outer column
  continue to work.

Tests:

* internal/compiler/resolve_test.go — new unit test covering
  narrowToInnermostScope across nil-rv, column-in-inner-scope (narrow),
  column-absent (fall back), and unknown-table (fall back).

* internal/endtoend/testdata/subquery_scope_4251/ — end-to-end fixture
  reproducing the exact query from the issue under postgresql/pgx/v5;
  generated code asserted against the golden files.

Verified locally:

* /tmp/repro4251 generate now returns EXIT=0 (was EXIT=1 with
  'relation "id" ambiguous').
* SELECT t1.id FROM t1, t2 WHERE id = $1 still correctly errors with
  'ambiguous'.
* Correlated subquery
  (SELECT SUM(total) FROM orders WHERE customer_id = c.id) still
  resolves cleanly.
* go test ./internal/compiler/... ./internal/sql/... ./internal/engine/...
  passes; gofmt -l and go vet clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@luongs3 luongs3 force-pushed the fix/4251-subquery-scope branch from 9780d84 to a47f6d3 Compare May 28, 2026 09:51
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.

Column reference is ambiguous in subquery

1 participant