-
Notifications
You must be signed in to change notification settings - Fork 605
Add derive based AST visitor #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f381f4
e882183
0cafaf9
3f4387a
89cb26a
6dc8367
387bdbe
971b422
c869d20
26b3289
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ version = "0.28.0" | |
authors = ["Andy Grove <[email protected]>"] | ||
homepage = "https://github.com/sqlparser-rs/sqlparser-rs" | ||
documentation = "https://docs.rs/sqlparser/" | ||
keywords = [ "ansi", "sql", "lexer", "parser" ] | ||
keywords = ["ansi", "sql", "lexer", "parser"] | ||
repository = "https://github.com/sqlparser-rs/sqlparser-rs" | ||
license = "Apache-2.0" | ||
include = [ | ||
|
@@ -23,6 +23,7 @@ default = ["std"] | |
std = [] | ||
# Enable JSON output in the `cli` example: | ||
json_example = ["serde_json", "serde"] | ||
visitor = ["sqlparser_derive"] | ||
|
||
[dependencies] | ||
bigdecimal = { version = "0.3", features = ["serde"], optional = true } | ||
|
@@ -32,6 +33,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } | |
# of dev-dependencies because of | ||
# https://github.com/rust-lang/cargo/issues/1596 | ||
serde_json = { version = "1.0", optional = true } | ||
sqlparser_derive = { version = "0.1", path = "derive", optional = true } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to myself: if we merge this PR I think we should document this feature flag -- it seems documentation is missing for the existing feature flags There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
[dev-dependencies] | ||
simple_logger = "4.0" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
[package] | ||
name = "sqlparser_derive" | ||
description = "proc macro for sqlparser" | ||
version = "0.1.0" | ||
authors = ["Andy Grove <[email protected]>"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 maybe this should be something different. I will think about a more appropriate author -- maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH I'm not sure as well. Theoretically I'd say that the owner/creator should be the author, but since this is an expansion, not sure whether put both of you or just the @tustvold There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is ok as is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed it to "sqlparser authors" in #775 |
||
homepage = "https://github.com/sqlparser-rs/sqlparser-rs" | ||
documentation = "https://docs.rs/sqlparser/" | ||
keywords = ["ansi", "sql", "lexer", "parser"] | ||
repository = "https://github.com/sqlparser-rs/sqlparser-rs" | ||
license = "Apache-2.0" | ||
include = [ | ||
"src/**/*.rs", | ||
"Cargo.toml", | ||
] | ||
edition = "2021" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
syn = "1.0" | ||
proc-macro2 = "1.0" | ||
quote = "1.0" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# SQL Parser Derive Macro | ||
|
||
## Visit | ||
|
||
This crate contains a procedural macro that can automatically derive implementations of the `Visit` trait | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self -- this should have a doc link back to the sqlparser crate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in #779 |
||
|
||
```rust | ||
#[derive(Visit)] | ||
struct Foo { | ||
boolean: bool, | ||
bar: Bar, | ||
} | ||
|
||
#[derive(Visit)] | ||
enum Bar { | ||
A(), | ||
B(String, bool), | ||
C { named: i32 }, | ||
} | ||
``` | ||
|
||
Will generate code akin to | ||
|
||
```rust | ||
impl Visit for Foo { | ||
fn visit<V: Visitor>(&self, visitor: &mut V) -> ControlFlow<V::Break> { | ||
self.boolean.visit(visitor)?; | ||
self.bar.visit(visitor)?; | ||
ControlFlow::Continue(()) | ||
} | ||
} | ||
|
||
impl Visit for Bar { | ||
fn visit<V: Visitor>(&self, visitor: &mut V) -> ControlFlow<V::Break> { | ||
match self { | ||
Self::A() => {} | ||
Self::B(_1, _2) => { | ||
_1.visit(visitor)?; | ||
_2.visit(visitor)?; | ||
} | ||
Self::C { named } => { | ||
named.visit(visitor)?; | ||
} | ||
} | ||
ControlFlow::Continue(()) | ||
} | ||
} | ||
``` | ||
|
||
Additionally certain types may wish to call a corresponding method on visitor before recursing | ||
|
||
```rust | ||
#[derive(Visit)] | ||
#[visit(with = "visit_expr")] | ||
enum Expr { | ||
A(), | ||
B(String, #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] ObjectName, bool), | ||
} | ||
``` | ||
|
||
Will generate | ||
|
||
```rust | ||
impl Visit for Bar { | ||
fn visit<V: Visitor>(&self, visitor: &mut V) -> ControlFlow<V::Break> { | ||
visitor.visit_expr(self)?; | ||
match self { | ||
Self::A() => {} | ||
Self::B(_1, _2, _3) => { | ||
_1.visit(visitor)?; | ||
visitor.visit_relation(_3)?; | ||
_2.visit(visitor)?; | ||
_3.visit(visitor)?; | ||
} | ||
} | ||
ControlFlow::Continue(()) | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
use proc_macro2::TokenStream; | ||
use quote::{format_ident, quote, quote_spanned, ToTokens}; | ||
use syn::spanned::Spanned; | ||
use syn::{ | ||
parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, | ||
Ident, Index, Lit, Meta, MetaNameValue, NestedMeta, | ||
}; | ||
|
||
/// Implementation of `[#derive(Visit)]` | ||
#[proc_macro_derive(Visit, attributes(visit))] | ||
alamb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub fn derive_visit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { | ||
// Parse the input tokens into a syntax tree. | ||
let input = parse_macro_input!(input as DeriveInput); | ||
let name = input.ident; | ||
|
||
let attributes = Attributes::parse(&input.attrs); | ||
// Add a bound `T: HeapSize` to every type parameter T. | ||
let generics = add_trait_bounds(input.generics); | ||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); | ||
|
||
let (pre_visit, post_visit) = attributes.visit(quote!(self)); | ||
let children = visit_children(&input.data); | ||
|
||
let expanded = quote! { | ||
// The generated impl. | ||
impl #impl_generics sqlparser::ast::Visit for #name #ty_generics #where_clause { | ||
fn visit<V: sqlparser::ast::Visitor>(&self, visitor: &mut V) -> ::std::ops::ControlFlow<V::Break> { | ||
#pre_visit | ||
#children | ||
#post_visit | ||
::std::ops::ControlFlow::Continue(()) | ||
} | ||
} | ||
}; | ||
|
||
proc_macro::TokenStream::from(expanded) | ||
} | ||
|
||
/// Parses attributes that can be provided to this macro | ||
/// | ||
/// `#[visit(leaf, with = "visit_expr")]` | ||
#[derive(Default)] | ||
struct Attributes { | ||
/// Content for the `with` attribute | ||
with: Option<Ident>, | ||
} | ||
|
||
impl Attributes { | ||
fn parse(attrs: &[Attribute]) -> Self { | ||
let mut out = Self::default(); | ||
for attr in attrs.iter().filter(|a| a.path.is_ident("visit")) { | ||
let meta = attr.parse_meta().expect("visit attribute"); | ||
match meta { | ||
Meta::List(l) => { | ||
for nested in &l.nested { | ||
match nested { | ||
NestedMeta::Meta(Meta::NameValue(v)) => out.parse_name_value(v), | ||
_ => panic!("Expected #[visit(key = \"value\")]"), | ||
} | ||
} | ||
} | ||
_ => panic!("Expected #[visit(...)]"), | ||
} | ||
} | ||
out | ||
} | ||
|
||
/// Updates self with a name value attribute | ||
fn parse_name_value(&mut self, v: &MetaNameValue) { | ||
if v.path.is_ident("with") { | ||
match &v.lit { | ||
Lit::Str(s) => self.with = Some(format_ident!("{}", s.value(), span = s.span())), | ||
_ => panic!("Expected a string value, got {}", v.lit.to_token_stream()), | ||
} | ||
return; | ||
} | ||
panic!("Unrecognised kv attribute {}", v.path.to_token_stream()) | ||
} | ||
|
||
/// Returns the pre and post visit token streams | ||
fn visit(&self, s: TokenStream) -> (Option<TokenStream>, Option<TokenStream>) { | ||
let pre_visit = self.with.as_ref().map(|m| { | ||
let m = format_ident!("pre_{}", m); | ||
quote!(visitor.#m(#s)?;) | ||
}); | ||
let post_visit = self.with.as_ref().map(|m| { | ||
let m = format_ident!("post_{}", m); | ||
quote!(visitor.#m(#s)?;) | ||
}); | ||
(pre_visit, post_visit) | ||
} | ||
} | ||
|
||
// Add a bound `T: Visit` to every type parameter T. | ||
fn add_trait_bounds(mut generics: Generics) -> Generics { | ||
for param in &mut generics.params { | ||
if let GenericParam::Type(ref mut type_param) = *param { | ||
type_param.bounds.push(parse_quote!(sqlparser::ast::Visit)); | ||
} | ||
} | ||
generics | ||
} | ||
|
||
// Generate the body of the visit implementation for the given type | ||
fn visit_children(data: &Data) -> TokenStream { | ||
match data { | ||
Data::Struct(data) => match &data.fields { | ||
Fields::Named(fields) => { | ||
let recurse = fields.named.iter().map(|f| { | ||
let name = &f.ident; | ||
let attributes = Attributes::parse(&f.attrs); | ||
let (pre_visit, post_visit) = attributes.visit(quote!(&self.#name)); | ||
quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(&self.#name, visitor)?; #post_visit) | ||
}); | ||
quote! { | ||
#(#recurse)* | ||
} | ||
} | ||
Fields::Unnamed(fields) => { | ||
let recurse = fields.unnamed.iter().enumerate().map(|(i, f)| { | ||
let index = Index::from(i); | ||
let attributes = Attributes::parse(&f.attrs); | ||
let (pre_visit, post_visit) = attributes.visit(quote!(&self.#index)); | ||
quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(&self.#index, visitor)?; #post_visit) | ||
}); | ||
quote! { | ||
#(#recurse)* | ||
} | ||
} | ||
Fields::Unit => { | ||
quote!() | ||
} | ||
}, | ||
Data::Enum(data) => { | ||
let statements = data.variants.iter().map(|v| { | ||
let name = &v.ident; | ||
match &v.fields { | ||
Fields::Named(fields) => { | ||
let names = fields.named.iter().map(|f| &f.ident); | ||
let visit = fields.named.iter().map(|f| { | ||
let name = &f.ident; | ||
let attributes = Attributes::parse(&f.attrs); | ||
let (pre_visit, post_visit) = attributes.visit(quote!(&#name)); | ||
quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(#name, visitor)?; #post_visit) | ||
}); | ||
|
||
quote!( | ||
Self::#name { #(#names),* } => { | ||
#(#visit)* | ||
} | ||
) | ||
} | ||
Fields::Unnamed(fields) => { | ||
let names = fields.unnamed.iter().enumerate().map(|(i, f)| format_ident!("_{}", i, span = f.span())); | ||
let visit = fields.unnamed.iter().enumerate().map(|(i, f)| { | ||
let name = format_ident!("_{}", i); | ||
let attributes = Attributes::parse(&f.attrs); | ||
let (pre_visit, post_visit) = attributes.visit(quote!(&#name)); | ||
quote_spanned!(f.span() => #pre_visit sqlparser::ast::Visit::visit(#name, visitor)?; #post_visit) | ||
}); | ||
|
||
quote! { | ||
Self::#name ( #(#names),*) => { | ||
#(#visit)* | ||
} | ||
} | ||
} | ||
Fields::Unit => { | ||
quote! { | ||
Self::#name => {} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
quote! { | ||
match self { | ||
#(#statements),* | ||
} | ||
} | ||
} | ||
Data::Union(_) => unimplemented!(), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,13 +17,17 @@ use core::fmt; | |
#[cfg(feature = "serde")] | ||
use serde::{Deserialize, Serialize}; | ||
|
||
#[cfg(feature = "visitor")] | ||
use sqlparser_derive::Visit; | ||
|
||
use crate::ast::ObjectName; | ||
|
||
use super::value::escape_single_quote_string; | ||
|
||
/// SQL data types | ||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] | ||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "visitor", derive(Visit))] | ||
pub enum DataType { | ||
/// Fixed-length character type e.g. CHARACTER(10) | ||
Character(Option<CharacterLength>), | ||
|
@@ -337,6 +341,7 @@ fn format_datetime_precision_and_tz( | |
/// guarantee compatibility with the input query we must maintain its exact information. | ||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] | ||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "visitor", derive(Visit))] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am a big fan of this method as a way to minimize maintenance burden on new contributions I picked a random struct and removed the I think the error is fairly clear 👍
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's also making it easier to keep up with the merge conflicts 😆 |
||
pub enum TimezoneInfo { | ||
/// No information about time zone. E.g., TIMESTAMP | ||
None, | ||
|
@@ -384,6 +389,7 @@ impl fmt::Display for TimezoneInfo { | |
/// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#exact-numeric-type | ||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] | ||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "visitor", derive(Visit))] | ||
pub enum ExactNumberInfo { | ||
/// No additional information e.g. `DECIMAL` | ||
None, | ||
|
@@ -414,6 +420,7 @@ impl fmt::Display for ExactNumberInfo { | |
/// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-length | ||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] | ||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "visitor", derive(Visit))] | ||
pub struct CharacterLength { | ||
/// Default (if VARYING) or maximum (if not VARYING) length | ||
pub length: u64, | ||
|
@@ -436,6 +443,7 @@ impl fmt::Display for CharacterLength { | |
/// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#char-length-units | ||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] | ||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "visitor", derive(Visit))] | ||
pub enum CharLengthUnits { | ||
/// CHARACTERS unit | ||
Characters, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also note to self
I tested using
cargo test --all --features=visitor
We need to add this feature to the CI tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that explains the poor coverage I guess
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I verified that the CI does run with
--all-features
: https://github.com/sqlparser-rs/sqlparser-rs/actions/runs/3794845435/jobs/6453351547https://github.com/sqlparser-rs/sqlparser-rs/blob/dec3c2b818bdedcc81f0e3990e2dd2fefcf9e872/.github/workflows/rust.yml#L67