From 97188517db99881954504279d134b61e8a8c6787 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 23 Feb 2026 14:45:23 +0100 Subject: [PATCH 1/6] Introduce Visitor#visit_select --- src/ast/mod.rs | 2 +- src/ast/query.rs | 4 ++-- src/ast/visitor.rs | 22 +++++++++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7b1e9447d5..f8e7a134c1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9946,7 +9946,7 @@ pub enum CreateFunctionBody { /// ``` /// /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#select_stmt - AsReturnSelect(Select), + AsReturnSelect(#[cfg_attr(feature = "visitor", visit(with = "visit_select"))] Select), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/query.rs b/src/ast/query.rs index ff617a38e4..0ed8bcbec6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -149,10 +149,10 @@ impl fmt::Display for ProjectionSelect { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SetExpr { /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) - Select(Box), /// Parenthesized SELECT subquery, which may include more set operations /// in its body and an optional ORDER BY / LIMIT. - Query(Box), + Query(#[cfg_attr(feature = "visitor", visit(with = "visit_query"))] Box), /// UNION/EXCEPT/INTERSECT of two queries /// A set operation combining two query expressions. SetOperation { diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 5d841655b5..9e40b4f6c7 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -17,7 +17,7 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor, Value}; +use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor, Value, Select}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for @@ -207,6 +207,16 @@ pub trait Visitor { ControlFlow::Continue(()) } + /// Invoked for any [Select] that appear in the AST before visiting children + fn pre_visit_select(&mut self, _select: &Select) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any [Select] that appear in the AST after visiting children + fn post_visit_select(&mut self, _select: &Select) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any relations (e.g. tables) that appear in the AST before visiting children fn pre_visit_relation(&mut self, _relation: &ObjectName) -> ControlFlow { ControlFlow::Continue(()) @@ -319,6 +329,16 @@ pub trait VisitorMut { ControlFlow::Continue(()) } + /// Invoked for any [Select] that appear in the AST before visiting children + fn pre_visit_select(&mut self, _select: &Select) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any [Select] that appear in the AST after visiting children + fn post_visit_select(&mut self, _select: &Select) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any relations (e.g. tables) that appear in the AST before visiting children fn pre_visit_relation(&mut self, _relation: &mut ObjectName) -> ControlFlow { ControlFlow::Continue(()) From 9bed977bbe0b77eabe8bba5862814d34b4a48e30 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 23 Feb 2026 14:47:50 +0100 Subject: [PATCH 2/6] Align visit order with parsed SQL --- src/ast/query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 0ed8bcbec6..9d0c598fe6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -156,12 +156,12 @@ pub enum SetExpr { /// UNION/EXCEPT/INTERSECT of two queries /// A set operation combining two query expressions. SetOperation { + /// Left operand of the set operation. + left: Box, /// The set operator used (e.g. `UNION`, `EXCEPT`). op: SetOperator, /// Optional quantifier (`ALL`, `DISTINCT`, etc.). set_quantifier: SetQuantifier, - /// Left operand of the set operation. - left: Box, /// Right operand of the set operation. right: Box, }, From 89a7789115e7466265924897bc9207e8cd11f3b0 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 23 Feb 2026 15:31:20 +0100 Subject: [PATCH 3/6] Cargo fmt --- src/ast/visitor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 9e40b4f6c7..891390c32d 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -17,7 +17,7 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Query, Statement, TableFactor, Value, Select}; +use crate::ast::{Expr, ObjectName, Query, Select, Statement, TableFactor, Value}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for From ae9bcafd4ad040315ebfcf67d70f81d0131e5f0d Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 23 Feb 2026 19:50:24 +0100 Subject: [PATCH 4/6] Fix mutability --- src/ast/visitor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 891390c32d..4e09c94064 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -330,12 +330,12 @@ pub trait VisitorMut { } /// Invoked for any [Select] that appear in the AST before visiting children - fn pre_visit_select(&mut self, _select: &Select) -> ControlFlow { + fn pre_visit_select(&mut self, _select: &mut Select) -> ControlFlow { ControlFlow::Continue(()) } /// Invoked for any [Select] that appear in the AST after visiting children - fn post_visit_select(&mut self, _select: &Select) -> ControlFlow { + fn post_visit_select(&mut self, _select: &mut Select) -> ControlFlow { ControlFlow::Continue(()) } From 9bdc892b7193cb4ad53bd99efcfbff3d7ce240e3 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 23 Feb 2026 20:49:40 +0100 Subject: [PATCH 5/6] Correct visit_select annotations --- src/ast/mod.rs | 2 +- src/ast/query.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f8e7a134c1..7b1e9447d5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9946,7 +9946,7 @@ pub enum CreateFunctionBody { /// ``` /// /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#select_stmt - AsReturnSelect(#[cfg_attr(feature = "visitor", visit(with = "visit_select"))] Select), + AsReturnSelect(Select), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/query.rs b/src/ast/query.rs index 9d0c598fe6..b4d3fdb2bc 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -149,10 +149,10 @@ impl fmt::Display for ProjectionSelect { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SetExpr { /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) - Select(#[cfg_attr(feature = "visitor", visit(with = "visit_select"))] Box), /// Parenthesized SELECT subquery, which may include more set operations /// in its body and an optional ORDER BY / LIMIT. - Query(#[cfg_attr(feature = "visitor", visit(with = "visit_query"))] Box), + Query(Box), /// UNION/EXCEPT/INTERSECT of two queries /// A set operation combining two query expressions. SetOperation { @@ -442,6 +442,7 @@ impl SelectModifiers { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "visitor", visit(with = "visit_select"))] pub struct Select { /// Token for the `SELECT` keyword pub select_token: AttachedToken, From e18b74e312f28d46ca5f626b9ceb93896456ac9e Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Tue, 24 Feb 2026 10:58:12 +0100 Subject: [PATCH 6/6] Test coverage --- src/ast/visitor.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 4e09c94064..5f9b374896 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -729,6 +729,16 @@ mod tests { ControlFlow::Continue(()) } + fn pre_visit_select(&mut self, select: &Select) -> ControlFlow { + self.visited.push(format!("PRE: SELECT: {select}")); + ControlFlow::Continue(()) + } + + fn post_visit_select(&mut self, select: &Select) -> ControlFlow { + self.visited.push(format!("POST: SELECT: {select}")); + ControlFlow::Continue(()) + } + fn pre_visit_relation(&mut self, relation: &ObjectName) -> ControlFlow { self.visited.push(format!("PRE: RELATION: {relation}")); ControlFlow::Continue(()) @@ -799,10 +809,12 @@ mod tests { vec![ "PRE: STATEMENT: SELECT * FROM table_name AS my_table", "PRE: QUERY: SELECT * FROM table_name AS my_table", + "PRE: SELECT: SELECT * FROM table_name AS my_table", "PRE: TABLE FACTOR: table_name AS my_table", "PRE: RELATION: table_name", "POST: RELATION: table_name", "POST: TABLE FACTOR: table_name AS my_table", + "POST: SELECT: SELECT * FROM table_name AS my_table", "POST: QUERY: SELECT * FROM table_name AS my_table", "POST: STATEMENT: SELECT * FROM table_name AS my_table", ], @@ -812,6 +824,7 @@ mod tests { vec![ "PRE: STATEMENT: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", "PRE: QUERY: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", + "PRE: SELECT: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", @@ -826,6 +839,7 @@ mod tests { "PRE: EXPR: t2.t1_id", "POST: EXPR: t2.t1_id", "POST: EXPR: t1.id = t2.t1_id", + "POST: SELECT: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", "POST: QUERY: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", "POST: STATEMENT: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", ], @@ -835,20 +849,24 @@ mod tests { vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "PRE: QUERY: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: SELECT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: QUERY: SELECT column FROM t2", + "PRE: SELECT: SELECT column FROM t2", "PRE: EXPR: column", "POST: EXPR: column", "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", "POST: TABLE FACTOR: t2", + "POST: SELECT: SELECT column FROM t2", "POST: QUERY: SELECT column FROM t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", + "POST: SELECT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "POST: QUERY: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", ], @@ -858,20 +876,24 @@ mod tests { vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "PRE: QUERY: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: SELECT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: QUERY: SELECT column FROM t2", + "PRE: SELECT: SELECT column FROM t2", "PRE: EXPR: column", "POST: EXPR: column", "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", "POST: TABLE FACTOR: t2", + "POST: SELECT: SELECT column FROM t2", "POST: QUERY: SELECT column FROM t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", + "POST: SELECT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "POST: QUERY: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", ], @@ -881,24 +903,30 @@ mod tests { vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", "PRE: QUERY: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", + "PRE: SELECT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: QUERY: SELECT column FROM t2", + "PRE: SELECT: SELECT column FROM t2", "PRE: EXPR: column", "POST: EXPR: column", "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", "POST: TABLE FACTOR: t2", + "POST: SELECT: SELECT column FROM t2", "POST: QUERY: SELECT column FROM t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", + "POST: SELECT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: SELECT: SELECT * FROM t3", "PRE: TABLE FACTOR: t3", "PRE: RELATION: t3", "POST: RELATION: t3", "POST: TABLE FACTOR: t3", + "POST: SELECT: SELECT * FROM t3", "POST: QUERY: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", ], @@ -912,6 +940,7 @@ mod tests { vec![ "PRE: STATEMENT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID", "PRE: QUERY: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID", + "PRE: SELECT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)", "PRE: TABLE FACTOR: monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)", "PRE: TABLE FACTOR: monthly_sales", "PRE: RELATION: monthly_sales", @@ -932,6 +961,7 @@ mod tests { "PRE: EXPR: 'APR'", "POST: EXPR: 'APR'", "POST: TABLE FACTOR: monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)", + "POST: SELECT: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d)", "PRE: EXPR: EMPID", "POST: EXPR: EMPID", "POST: QUERY: SELECT * FROM monthly_sales PIVOT(SUM(a.amount) FOR a.MONTH IN ('JAN', 'FEB', 'MAR', 'APR')) AS p (c, d) ORDER BY EMPID",