Skip to content

Register a bare query operand as a subquery scope - #10

Merged
ravenac95 merged 7 commits into
mainfrom
register-bare-query-operands
Oct 5, 2026
Merged

ravenac95 merged 7 commits into
mainfrom
register-bare-query-operands

Conversation

@ravenac95

Copy link
Copy Markdown
Member

build_scope gives no scope to a query that is a plain operand of an expression, such as the argument of ARRAY(SELECT x FROM t). That query's tables and columns are invisible to everything built on the scope tree.

SELECT ARRAY(SELECT x FROM u) FROM t
-- before: subquery_scopes is empty, so `u` is never seen
-- after:  one Subquery scope with source `u`

It happens because of two gaps:

  • build_scope registers a subquery scope only for an unaliased Subquery wrapper and for EXISTS, IN and ANY/ALL operands.
  • walk_in_scope stops at a bare Select/Union/Intersect/Except.

sqlglot treats every unwrapped query in scope as a subquery, so this is a parity fix.

Where the shape occurs

  • ARRAY(<query>) parses to a function with a bare query argument in every dialect, including when the argument is a UNION.
  • ClickHouse also accepts a bare query as the argument of an ordinary function (f(SELECT ...)).
  • The query can sit anywhere a scope's expressions can: projections, WHERE, GROUP BY, HAVING, ORDER BY, QUALIFY and JOIN conditions.

What changes

  • New scopes: collect_subqueries_in_expr now also registers every bare query that is a child of a node the walk yields. It uses the same branch/build_child path as the other operands, so ScopeVisitor reports the new scopes too.
  • Dedup: the existing pointer dedup still gives each IN/EXISTS/Subquery/ANY/ALL operand exactly one scope.
  • Unchanged: FROM/JOIN items, CTE bodies and set-operation branches, and walk_in_scope, which still stops at bare queries. The small helper is_bare_query is shared with should_stop_at.
  • Behaviour change: ANY/ALL now register their operand only when it is a query. x = ANY(ARRAY(SELECT ...)) used to produce an empty scope for the ARRAY(...) operand plus the inner query's scope. It now yields one subquery scope, with the inner query's sources.
  • Out of scope: subqueries in a set operation's own trailing ORDER BY/LIMIT.

Tests

  • Function-argument queries give exactly one subquery scope with the right sources. These cover ARRAY(SELECT ...) in Generic, BigQuery and PostgreSQL, a UNION argument, and a ClickHouse f(SELECT ...), in the projection, WHERE, GROUP BY, HAVING, ORDER BY, QUALIFY and JOIN ON.
  • ANY(ARRAY(SELECT ...)) and ALL(ARRAY(SELECT ...)) give exactly one subquery scope.
  • Nested bare operands mixed with scalar, IN and EXISTS subqueries each register once.
  • A ScopeVisitor test shows the new scope reported as ScopeType::Subquery.
  • No existing expectation changed.

Run locally on this head, which includes main: cargo fmt --all -- --check, make check-consistency, clippy -D warnings on the ffi and wasm crates, and make test-rust-verify-core (1,330 lib tests and the integration suites) all pass.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 7 commits October 5, 2026 16:08
build_scope only registered a subquery wrapper, EXISTS, IN and ANY/ALL
operands, and walk_in_scope stops at a bare Select/Union/Intersect/Except.
A query that is a plain operand of an expression, such as the argument of
ARRAY(SELECT ...), therefore got no scope, and its tables and columns were
invisible to everything built on the scope tree. sqlglot treats every
unwrapped query in scope as a subquery.

collect_subqueries_in_expr now also registers each bare query among the
children of a node the walk yields, through the same branch/build_child
path, so ScopeVisitor reports it. The existing pointer dedup keeps each
operand to a single scope. FROM/JOIN items, CTE bodies and set operation
branches are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fa6xBWiFS7JFH5kt1epZ7o
…ry public

enter_scope now receives the node from the expression given to
build_scope_with instead of the scope's own copy, matching reference and
skipped, so a visitor can link nodes of the input to the scopes built for
them. scope_query is public and re-exported alongside build_scope_with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fa6xBWiFS7JFH5kt1epZ7o
The quantified-expression arm registered its operand as a scope whatever its
shape, so `x = ANY(ARRAY(SELECT y FROM u))` produced an empty scope for the
ARRAY(...) function in addition to the scope of the inner query that the bare
operand arm now registers. Register the operand only when it is itself a
query; an array or value operand is walked like any other expression, so the
statement yields one subquery scope with the inner query's sources.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fa6xBWiFS7JFH5kt1epZ7o
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fa6xBWiFS7JFH5kt1epZ7o
@ravenac95
ravenac95 merged commit d6533d5 into main Oct 5, 2026
8 checks passed
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.

2 participants