From a8e07c58c71b09d9d0418239512bca50b2da53ec Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Sun, 15 Nov 2026 06:58:31 +0200 Subject: [PATCH] AST: expose constraint characteristics visitor hooks --- AGENTS.md | 6 + README.md | 6 + src/ast/ddl.rs | 1 + src/ast/visitor.rs | 41 ++- ...isitor_constraint_characteristics_tests.rs | 254 ++++++++++++++++++ 5 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 src/ast/visitor_constraint_characteristics_tests.rs diff --git a/AGENTS.md b/AGENTS.md index 68081ddc9a..01786fccd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,12 @@ and `Dialect::supports_constraint_reliance` capability. Keep RELY/NORELY distinc from enforcement and deferrability, preserve absent-field serde compatibility and statement boundaries, and never infer optimizer or execution semantics from syntax. +Constraint-characteristics visitor hooks belong on the native characteristics +type, covering every derived placement without a container-specific inventory. +Preserve default-Continue behavior, early-break propagation, mutable hooks and +the actual borrowed instance; traversal must not clone characteristics or infer +constraint semantics. + Run the following commands before you commit to ensure the change will pass the CI process: ```bash cargo test --all-features diff --git a/README.md b/README.md index 1e4227f4ee..335b218a22 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ The following optional [crate features](https://doc.rust-lang.org/cargo/referen * `visitor`: Adds a `Visitor` capable of recursively walking the AST tree. * `recursive-protection` (enabled by default), uses [recursive](https://docs.rs/recursive/latest/recursive/) for stack overflow protection. +`Visitor` and `VisitorMut` expose pre/post hooks for each native +`ConstraintCharacteristics` instance, including characteristics nested in view +columns, domains, Hive definitions and triggers. Existing visitors default to +continuing traversal; hooks may stop it with `ControlFlow::Break`. These callbacks +add no constraint execution or optimizer semantics and change no serialized AST. + ## Syntax vs Semantics This crate provides only a syntax parser, and tries to avoid applying diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 2a4288f608..efcb15d1d7 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2230,6 +2230,7 @@ pub(crate) fn display_option_spaced(option: &Option) -> impl #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "visitor", visit(with = "visit_constraint_characteristics"))] pub struct ConstraintCharacteristics { /// `[ DEFERRABLE | NOT DEFERRABLE ]` pub deferrable: Option, diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 30673dfa03..4a1a2345b7 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -17,7 +17,10 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Query, Select, Statement, TableFactor, ValueWithSpan}; +use crate::ast::{ + ConstraintCharacteristics, Expr, ObjectName, Query, Select, Statement, TableFactor, + ValueWithSpan, +}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for @@ -197,6 +200,22 @@ pub trait Visitor { /// boxing it with `Box` to minimize stack usage. type Break; + /// Invoked for constraint characteristics before visiting their children. + fn pre_visit_constraint_characteristics( + &mut self, + _characteristics: &ConstraintCharacteristics, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for constraint characteristics after visiting their children. + fn post_visit_constraint_characteristics( + &mut self, + _characteristics: &ConstraintCharacteristics, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any queries that appear in the AST before visiting children fn pre_visit_query(&mut self, _query: &Query) -> ControlFlow { ControlFlow::Continue(()) @@ -319,6 +338,22 @@ pub trait VisitorMut { /// boxing it with `Box` to minimize stack usage. type Break; + /// Invoked for constraint characteristics before visiting their children. + fn pre_visit_constraint_characteristics( + &mut self, + _characteristics: &mut ConstraintCharacteristics, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for constraint characteristics after visiting their children. + fn post_visit_constraint_characteristics( + &mut self, + _characteristics: &mut ConstraintCharacteristics, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any queries that appear in the AST before visiting children fn pre_visit_query(&mut self, _query: &mut Query) -> ControlFlow { ControlFlow::Continue(()) @@ -701,6 +736,10 @@ where ControlFlow::Continue(()) } +#[cfg(test)] +#[path = "visitor_constraint_characteristics_tests.rs"] +mod constraint_characteristics_tests; + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/visitor_constraint_characteristics_tests.rs b/src/ast/visitor_constraint_characteristics_tests.rs new file mode 100644 index 0000000000..a309fca233 --- /dev/null +++ b/src/ast/visitor_constraint_characteristics_tests.rs @@ -0,0 +1,254 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use super::*; +use crate::ast::{ + ColumnOption, ColumnOptions, ConstraintUsingIndex, CreateDomain, HiveDistributionStyle, Ident, + SetExpr, TableConstraint, ViewColumnDef, +}; +use crate::dialect::{GenericDialect, PostgreSqlDialect}; +use crate::parser::{Parser, ParserError}; + +type TestError = Box; +type TestResult = Result<(), TestError>; +type NativeContainer = (&'static str, Statement, usize); + +fn statement(sql: &str) -> Result { + Ok(Parser::parse_sql(&GenericDialect {}, sql)?.remove(0)) +} + +// Native combinations need not be emitted by a dialect's grammar. Every derived +// placement must expose its actual characteristics to a consumer's visitor. +fn native_containers() -> Result, TestError> { + let inline = statement("CREATE TABLE t(id INT PRIMARY KEY NOT ENFORCED RELY)")?; + let Statement::CreateTable(template) = &inline else { + return Err("unexpected native AST fixture".into()); + }; + let column = template.columns[0].clone(); + let mut view = statement("CREATE VIEW v AS SELECT 1")?; + let Statement::CreateView(create) = &mut view else { + return Err("unexpected native AST fixture".into()); + }; + create.columns.push(ViewColumnDef { + name: Ident::new("id"), + data_type: None, + options: Some(ColumnOptions::SpaceSeparated(vec![column.options[0] + .option + .clone()])), + }); + let mut partitioned = template.clone(); + partitioned.columns.clear(); + partitioned.hive_distribution = HiveDistributionStyle::PARTITIONED { + columns: vec![column.clone()], + }; + let mut skewed = template.clone(); + skewed.columns.clear(); + skewed.hive_distribution = HiveDistributionStyle::SKEWED { + columns: vec![column.clone()], + on: vec![column.clone()], + stored_as_directories: false, + }; + let Statement::Query(mut query) = statement("SELECT 1")? else { + return Err("unexpected native AST fixture".into()); + }; + query.body = Box::new(SetExpr::Insert(view.clone())); + let mut nested = template.clone(); + nested.columns.clear(); + nested.query = Some(query); + let domain = Statement::CreateDomain(CreateDomain { + name: vec![Ident::new("d")].into(), + data_type: column.data_type.clone(), + collation: None, + default: None, + constraints: vec![TableConstraint::PrimaryKeyUsingIndex( + ConstraintUsingIndex { + name: None, + index_name: Ident::new("i"), + characteristics: Some(ConstraintCharacteristics { + rely: Some(true), + ..Default::default() + }), + }, + )], + }); + let mut trigger = Parser::parse_sql( + &PostgreSqlDialect {}, + "CREATE CONSTRAINT TRIGGER trg BEFORE INSERT ON t DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION f", + )? + .remove(0); + let Statement::CreateTrigger(create) = &mut trigger else { + return Err("unexpected native AST fixture".into()); + }; + create.characteristics.as_mut().unwrap().rely = Some(true); + Ok(vec![ + ("inline primary key", inline, 1), + ( + "unique and foreign key", + statement("CREATE TABLE t(u INT UNIQUE NOT ENFORCED NORELY, f INT REFERENCES other(id) NOT ENFORCED RELY)")?, + 2, + ), + ( + "table primary key", + statement("CREATE TABLE t(id INT, PRIMARY KEY(id) NOT ENFORCED RELY)")?, + 1, + ), + ("view column options", view, 1), + ("Hive partition columns", Statement::CreateTable(partitioned), 1), + ("Hive skew columns and ON", Statement::CreateTable(skewed), 2), + ("domain constraint using index", domain, 1), + ("trigger characteristics", trigger, 1), + ("nested native DDL", Statement::CreateTable(nested), 1), + ]) +} + +#[derive(Default)] +struct Counts { + pre: Vec>, + post: Vec>, +} + +impl Visitor for Counts { + type Break = (); + + fn pre_visit_constraint_characteristics( + &mut self, + value: &ConstraintCharacteristics, + ) -> ControlFlow { + self.pre.push(value.rely); + ControlFlow::Continue(()) + } + + fn post_visit_constraint_characteristics( + &mut self, + value: &ConstraintCharacteristics, + ) -> ControlFlow { + self.post.push(value.rely); + ControlFlow::Continue(()) + } +} + +#[test] +fn constraint_characteristics_hooks_reach_all_native_containers() -> TestResult { + for (name, node, expected) in native_containers()? { + let mut counts = Counts::default(); + assert!(Visit::visit(&node, &mut counts).is_continue(), "{name}"); + assert_eq!(counts.pre.len(), expected, "{name}"); + assert_eq!(counts.post, counts.pre, "{name}"); + assert!(counts.pre.iter().all(Option::is_some), "{name}"); + } + Ok(()) +} + +#[test] +fn constraint_characteristics_break_preserves_actual_borrowed_identity() -> TestResult { + let node = statement("CREATE TABLE t(id INT PRIMARY KEY NOT ENFORCED RELY)")?; + let Statement::CreateTable(create) = &node else { + return Err("unexpected native AST fixture".into()); + }; + let ColumnOption::PrimaryKey(key) = &create.columns[0].options[0].option else { + return Err("unexpected native AST fixture".into()); + }; + struct Permit<'a>(&'a ConstraintCharacteristics); + impl Visitor for Permit<'_> { + type Break = &'static str; + fn pre_visit_constraint_characteristics( + &mut self, + value: &ConstraintCharacteristics, + ) -> ControlFlow { + if std::ptr::eq(self.0, value) { + ControlFlow::Continue(()) + } else { + ControlFlow::Break("another instance") + } + } + } + let permitted = key.characteristics.as_ref().unwrap(); + assert!(Visit::visit(&node, &mut Permit(permitted)).is_continue()); + assert_eq!( + Visit::visit(&node.clone(), &mut Permit(permitted)), + ControlFlow::Break("another instance") + ); + Ok(()) +} + +#[test] +fn constraint_characteristics_mutable_hooks_and_post_break_propagate() -> TestResult { + struct Mutate { + pre: usize, + post: usize, + } + impl VisitorMut for Mutate { + type Break = (); + fn pre_visit_constraint_characteristics( + &mut self, + value: &mut ConstraintCharacteristics, + ) -> ControlFlow { + self.pre += 1; + value.rely = Some(false); + ControlFlow::Continue(()) + } + fn post_visit_constraint_characteristics( + &mut self, + value: &mut ConstraintCharacteristics, + ) -> ControlFlow { + self.post += 1; + assert_eq!(value.rely, Some(false)); + ControlFlow::Break(()) + } + } + let mut node = statement( + "CREATE TABLE t(a INT UNIQUE NOT ENFORCED RELY, b INT UNIQUE NOT ENFORCED RELY)", + )?; + let mut visitor = Mutate { pre: 0, post: 0 }; + assert!(VisitMut::visit(&mut node, &mut visitor).is_break()); + assert_eq!((visitor.pre, visitor.post), (1, 1)); + let mut after = Counts::default(); + let _ = Visit::visit(&node, &mut after); + assert_eq!(after.pre, [Some(false), Some(true)]); + Ok(()) +} + +#[test] +fn constraint_characteristics_default_hooks_preserve_existing_visitors() -> TestResult { + #[derive(Default, PartialEq, Debug)] + struct Existing { + statements: usize, + expressions: usize, + } + impl Visitor for Existing { + type Break = (); + fn pre_visit_statement(&mut self, _: &Statement) -> ControlFlow { + self.statements += 1; + ControlFlow::Continue(()) + } + fn pre_visit_expr(&mut self, _: &Expr) -> ControlFlow { + self.expressions += 1; + ControlFlow::Continue(()) + } + } + let plain = statement("CREATE TABLE t(id INT PRIMARY KEY)")?; + let reliant = statement("CREATE TABLE t(id INT PRIMARY KEY NOT ENFORCED RELY)")?; + let (mut before, mut after) = (Existing::default(), Existing::default()); + assert!(Visit::visit(&plain, &mut before).is_continue()); + assert!(Visit::visit(&reliant, &mut after).is_continue()); + assert_eq!(before, after); + let query = statement("SELECT 1 FROM t WHERE id = 2")?; + let mut counts = Counts::default(); + assert!(Visit::visit(&query, &mut counts).is_continue()); + assert!(counts.pre.is_empty() && counts.post.is_empty()); + Ok(()) +}