Skip to content

Commit ab477e9

Browse files
def-claude
andcommitted
sql: parenthesize quantified-subquery receivers of .field/.*
`write_dot_receiver` listed `AnySubquery`/`AllSubquery` as safe `.` receivers, but they print as `<expr> <op> ANY/ALL (<query>)` — the trailing `(<query>)` is only a sub-part of the expression, so a following `.x`/`.*` binds to that inner subquery rather than the whole quantified comparison. Printed bare, a `Nested`-stripped `FieldAccess`/`WildcardAccess` over one of these re-associated on reparse (`(a = ANY (SELECT b FROM t)).c` -> `a = ANY (SELECT b FROM t).c`), which also surfaced inside `BETWEEN` bounds as an `Expected AND` parse error. Drop both from the safe set so they are parenthesized. The single-primary subquery forms (`Subquery`, `ArraySubquery`, `ListSubquery`, `MapSubquery`) stay safe: they are one `(…)`/`ARRAY(…)` token sequence, so a trailing dot attaches to the whole thing. The parser only builds these accesses over a parenthesized receiver (wrapped in `Expr::Nested`), so the change is churn-free and the datadriven suites are unchanged. Tests: extends `postfix_access_receiver_reparenthesized_after_nested_stripped` with the quantified-subquery receiver cases (incl. one inside a `BETWEEN` bound). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 96fda3f commit ab477e9

2 files changed

Lines changed: 13 additions & 2 deletions

File tree

src/sql-parser/src/ast/defs/expr.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,13 @@ impl_display_t!(Expr);
572572
/// is a `Nested`-stripped AST and must be re-parenthesized. A `FieldAccess` /
573573
/// `WildcardAccess` receiver *is* safe, because its own printing already
574574
/// parenthesizes a bare-name base (`(a).b.c`), so the chain stays self-delimiting.
575+
///
576+
/// The quantified-subquery forms (`AnySubquery`/`AllSubquery`, printed
577+
/// `<expr> <op> ANY (<query>)`) are likewise *not* safe: they end in a `(query)`
578+
/// that is only a sub-part, so a trailing `.x`/`.*` binds to that inner subquery
579+
/// rather than the whole expression. (Contrast `Subquery`/`ArraySubquery`/… which
580+
/// are a single `(…)`/`ARRAY(…)` primary, so a trailing dot attaches to the whole
581+
/// thing.)
575582
fn write_dot_receiver<W: fmt::Write, T: AstInfo>(f: &mut AstFormatter<W>, expr: &Expr<T>) {
576583
let safe = matches!(
577584
expr,
@@ -584,8 +591,6 @@ fn write_dot_receiver<W: fmt::Write, T: AstInfo>(f: &mut AstFormatter<W>, expr:
584591
| Expr::Case { .. }
585592
| Expr::Exists(_)
586593
| Expr::Subquery(_)
587-
| Expr::AnySubquery { .. }
588-
| Expr::AllSubquery { .. }
589594
| Expr::Array(_)
590595
| Expr::ArraySubquery(_)
591596
| Expr::List(_)

src/sql-parser/tests/sqlparser_common.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,12 @@ fn postfix_access_receiver_reparenthesized_after_nested_stripped() {
10271027
"SELECT (CAST(a AS int4))[1]",
10281028
"SELECT (a[1])[2]",
10291029
"SELECT (a + b)[1]",
1030+
// quantified-subquery dot receiver: the trailing `(query)` is a sub-part,
1031+
// so `.x`/`.*` would bind to it rather than the whole quantified expr.
1032+
"SELECT (a = ANY (SELECT b FROM t)).c",
1033+
"SELECT (a = ANY (SELECT b FROM t)).*",
1034+
"SELECT (a = ALL (SELECT b FROM t)).c",
1035+
"SELECT x BETWEEN (0 = ANY (SELECT b FROM t)).c AND y",
10301036
// these are parser-shaped and must remain stable (no spurious parens)
10311037
"SELECT (x).a.b",
10321038
"SELECT a.b[1]",

0 commit comments

Comments
 (0)