Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,7 @@ pub(crate) fn display_option_spaced<T: fmt::Display>(option: &Option<T>) -> 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<bool>,
Expand Down
41 changes: 40 additions & 1 deletion src/ast/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Self::Break> {
ControlFlow::Continue(())
}

/// Invoked for constraint characteristics after visiting their children.
fn post_visit_constraint_characteristics(
&mut self,
_characteristics: &ConstraintCharacteristics,
) -> ControlFlow<Self::Break> {
ControlFlow::Continue(())
}

/// Invoked for any queries that appear in the AST before visiting children
fn pre_visit_query(&mut self, _query: &Query) -> ControlFlow<Self::Break> {
ControlFlow::Continue(())
Expand Down Expand Up @@ -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<Self::Break> {
ControlFlow::Continue(())
}

/// Invoked for constraint characteristics after visiting their children.
fn post_visit_constraint_characteristics(
&mut self,
_characteristics: &mut ConstraintCharacteristics,
) -> ControlFlow<Self::Break> {
ControlFlow::Continue(())
}

/// Invoked for any queries that appear in the AST before visiting children
fn pre_visit_query(&mut self, _query: &mut Query) -> ControlFlow<Self::Break> {
ControlFlow::Continue(())
Expand Down Expand Up @@ -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::*;
Expand Down
254 changes: 254 additions & 0 deletions src/ast/visitor_constraint_characteristics_tests.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>;
type TestResult = Result<(), TestError>;
type NativeContainer = (&'static str, Statement, usize);

fn statement(sql: &str) -> Result<Statement, ParserError> {
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<Vec<NativeContainer>, 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<Option<bool>>,
post: Vec<Option<bool>>,
}

impl Visitor for Counts {
type Break = ();

fn pre_visit_constraint_characteristics(
&mut self,
value: &ConstraintCharacteristics,
) -> ControlFlow<Self::Break> {
self.pre.push(value.rely);
ControlFlow::Continue(())
}

fn post_visit_constraint_characteristics(
&mut self,
value: &ConstraintCharacteristics,
) -> ControlFlow<Self::Break> {
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<Self::Break> {
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::Break> {
self.pre += 1;
value.rely = Some(false);
ControlFlow::Continue(())
}
fn post_visit_constraint_characteristics(
&mut self,
value: &mut ConstraintCharacteristics,
) -> ControlFlow<Self::Break> {
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::Break> {
self.statements += 1;
ControlFlow::Continue(())
}
fn pre_visit_expr(&mut self, _: &Expr) -> ControlFlow<Self::Break> {
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(())
}
Loading