Skip to content
Open
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
149 changes: 66 additions & 83 deletions derive/src/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::collections::HashSet;
use syn::{
braced,
parse::{Parse, ParseStream},
Error, Expr, File, FnArg, Ident, Item, LitBool, LitChar, Pat, ReceiverKind, ReturnType,
Error, Expr, FnArg, Ident, ItemTrait, LitBool, LitChar, Pat, ReceiverKind, ReturnType,
Signature, Token, TraitItem, Type,
};

Expand Down Expand Up @@ -110,22 +110,56 @@ impl Parse for DeriveDialectInput {
}
}

/// Entry point for the `derive_dialect!` macro
pub(crate) fn derive_dialect(input: DeriveDialectInput) -> proc_macro::TokenStream {
/// Pass the trait's signatures to each derived dialect without reading source files.
pub(crate) fn dialect_methods(item: ItemTrait) -> TokenStream {
let signatures = item.items.iter().filter_map(|item| match item {
TraitItem::Fn(method) => Some(&method.sig),
_ => None,
});
quote! {
#item

#[doc(hidden)]
#[macro_export]
macro_rules! __sqlparser_derive_dialect {
($($args:tt)*) => {
$crate::dialect::__derive_dialect! {
trait Dialect { #(#signatures;)* }
$($args)*
}
};
}

#[doc(hidden)]
pub use __sqlparser_derive_dialect as __derive_dialect_with_methods;
}
}

pub(crate) struct DeriveDialectWithMethods {
dialect: ItemTrait,
input: DeriveDialectInput,
}

impl Parse for DeriveDialectWithMethods {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
dialect: input.parse()?,
input: input.parse()?,
})
}
}

/// Entry point after the trait has supplied its method signatures.
pub(crate) fn derive_dialect(input: DeriveDialectWithMethods) -> proc_macro::TokenStream {
match derive_dialect_inner(input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}

fn derive_dialect_inner(input: DeriveDialectInput) -> syn::Result<TokenStream> {
let call_site = proc_macro2::Span::call_site();

let source = read_dialect_mod_file()
.map_err(|e| Error::new(call_site, format!("Failed to read dialect/mod.rs: {e}")))?;
let file: File = syn::parse_str::<File>(&source)
.map_err(|e| Error::new(call_site, format!("Failed to parse source: {e}")))?;
let methods = extract_dialect_methods(&file)?;
fn derive_dialect_inner(input: DeriveDialectWithMethods) -> syn::Result<TokenStream> {
let methods = extract_dialect_methods(&input.dialect, input.input.name.span());
let input = input.input;

// Validate overrides
let method_names: HashSet<_> = methods.iter().map(|m| m.name.to_string()).collect();
Expand Down Expand Up @@ -254,86 +288,35 @@ fn extract_param_names(sig: &Signature) -> Vec<&Ident> {
.collect()
}

/// Read the `dialect/mod.rs` file that contains the Dialect trait.
///
/// Searches for the file in the following order:
/// 1. `$CARGO_MANIFEST_DIR/src/dialect/mod.rs` - works when the macro is
/// invoked from within the `sqlparser` crate itself (e.g. in tests).
/// 2. `<sqlparser_derive dir>/../src/dialect/mod.rs` - works when
/// `sqlparser_derive` lives in a workspace alongside the main crate
/// (the standard `derive/` layout).
/// 3. Sibling directories of the compiled `sqlparser_derive` crate in the
/// Cargo registry - works when an external crate uses `derive_dialect!`
/// via a registry dependency.
fn read_dialect_mod_file() -> Result<String, String> {
use std::path::{Path, PathBuf};

const DERIVE_CRATE_DIR: &str = env!("CARGO_MANIFEST_DIR");
let derive_dir = Path::new(DERIVE_CRATE_DIR);
let mut candidates: Vec<PathBuf> = Vec::new();

// The crate being compiled (eg: within sqlparser).
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
candidates.push(Path::new(&manifest_dir).join("src/dialect/mod.rs"));
}
// Workspace layout: the main crate is the parent of `derive/`.
candidates.push(derive_dir.join("../src/dialect/mod.rs"));

// Cargo registry: look for sibling `sqlparser-*` directories (prefer newest).
if let Some(parent) = derive_dir.parent() {
if let Ok(entries) = std::fs::read_dir(parent) {
let mut siblings: Vec<_> = entries
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.starts_with("sqlparser-") && !name.starts_with("sqlparser-derive")
})
.collect();
siblings.sort_by_key(|e| std::cmp::Reverse(e.file_name()));
candidates.extend(
siblings
.into_iter()
.map(|e| e.path().join("src/dialect/mod.rs")),
);
}
}
for path in &candidates {
if let Ok(content) = std::fs::read_to_string(path) {
return Ok(content);
}
}
Err(format!(
"Could not find `sqlparser` dialect/mod.rs file. \
Searched in $CARGO_MANIFEST_DIR/src/dialect/mod.rs and \
the `sqlparser_derive` crate at {DERIVE_CRATE_DIR}"
))
}

/// Extract all methods from the `Dialect` trait (excluding `dialect` for TypeId)
fn extract_dialect_methods(file: &File) -> Result<Vec<DialectMethod>, Error> {
let dialect_trait = file
.items
.iter()
.find_map(|item| match item {
Item::Trait(t) if t.ident == "Dialect" => Some(t),
_ => None,
})
.ok_or_else(|| Error::new(proc_macro2::Span::call_site(), "Dialect trait not found"))?;

/// Extract all methods from the supplied trait (excluding `dialect` for TypeId).
fn extract_dialect_methods(
dialect_trait: &ItemTrait,
call_site: proc_macro2::Span,
) -> Vec<DialectMethod> {
let mut methods: Vec<_> = dialect_trait
.items
.iter()
.filter_map(|item| match item {
TraitItem::Fn(m) if m.sig.ident != "dialect" => Some(DialectMethod {
name: m.sig.ident.clone(),
signature: m.sig.clone(),
}),
TraitItem::Fn(m) if m.sig.ident != "dialect" => {
let mut signature = m.sig.clone();
// Override expressions refer to parameters at the invocation site.
for arg in &mut signature.inputs {
if let FnArg::Typed(arg) = arg {
if let Pat::Ident(pat) = arg.pat.as_mut() {
pat.ident.set_span(call_site);
}
}
}
Some(DialectMethod {
name: m.sig.ident.clone(),
signature,
})
}
_ => None,
})
.collect();
methods.sort_by_key(|m| m.name.to_string());
Ok(methods)
methods
}

/// Check if a method signature is `fn name(&self) -> bool`
Expand Down
23 changes: 22 additions & 1 deletion derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,29 @@ pub fn derive_visit_immutable(input: proc_macro::TokenStream) -> proc_macro::Tok
}

/// Procedural macro for deriving new SQL dialects.
///
/// Enable the `derive-dialect` feature on the `sqlparser` dependency.
#[proc_macro]
pub fn derive_dialect(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as dialect::DeriveDialectInput);
let input = proc_macro2::TokenStream::from(input);
quote!(::sqlparser::dialect::__derive_dialect_with_methods!(#input);).into()
}

/// Capture method signatures from the Dialect trait definition.
#[doc(hidden)]
#[proc_macro_attribute]
pub fn dialect_methods(
_args: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = parse_macro_input!(item as syn::ItemTrait);
dialect::dialect_methods(item).into()
}

/// Expand a dialect using the method signatures supplied by sqlparser.
#[doc(hidden)]
#[proc_macro]
pub fn __derive_dialect(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as dialect::DeriveDialectWithMethods);
dialect::derive_dialect(input)
}
5 changes: 5 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ pub use self::teradata::TeradataDialect;
#[cfg(feature = "derive-dialect")]
pub use sqlparser_derive::derive_dialect;

#[cfg(feature = "sqlparser_derive")]
#[doc(hidden)]
pub use sqlparser_derive::__derive_dialect;

use crate::ast::{ColumnOption, DataType, Expr, GranteesType, Ident, ObjectNamePart, Statement};
pub use crate::keywords;
use crate::keywords::Keyword;
Expand Down Expand Up @@ -231,6 +235,7 @@ pub enum BindPlaceholderStyle {
/// ```
///
/// [module level documentation]: crate
#[cfg_attr(feature = "sqlparser_derive", sqlparser_derive::dialect_methods)]
pub trait Dialect: Debug + Any {
/// Determine the [`TypeId`] of this dialect.
///
Expand Down
20 changes: 20 additions & 0 deletions tests/sqlparser_derive_dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,23 @@ fn test_literal_leading_bool_expression_override() {
Some(sqlparser::dialect::BindPlaceholderStyle::DollarNumbered)
);
}

#[test]
fn test_direct_and_module_macro_paths() {
sqlparser_derive::derive_dialect!(
DirectDialect,
MySqlDialect,
overrides = { is_identifier_start = '_' == ch || ch.is_ascii_alphabetic() }
);
sqlparser::dialect::derive_dialect!(
ModuleDialect,
MySqlDialect,
overrides = { supports_string_literal_backslash_escape = false }
);

let direct = DirectDialect::new();
assert!(direct.is_identifier_start('_'));
assert!(!direct.is_identifier_start('1'));
assert!(direct.supports_string_literal_backslash_escape());
assert!(!ModuleDialect::new().supports_string_literal_backslash_escape());
}
Loading