From bd48c0cccfd73c400fd9a421e7da2bf4cb87516d Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:14:30 +0200 Subject: [PATCH 1/2] Add ordered bind placeholder dialect metadata --- derive/src/dialect.rs | 40 +++++++++------------ src/dialect/mod.rs | 74 +++++++++++++++++++++++++++++++++++++-- src/dialect/mysql.rs | 6 +++- src/dialect/postgresql.rs | 6 +++- src/dialect/sqlite.rs | 6 +++- 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/derive/src/dialect.rs b/derive/src/dialect.rs index af17067acb..ce808e7e25 100644 --- a/derive/src/dialect.rs +++ b/derive/src/dialect.rs @@ -23,15 +23,15 @@ use std::collections::HashSet; use syn::{ braced, parse::{Parse, ParseStream}, - Error, File, FnArg, Ident, Item, LitBool, LitChar, Pat, ReceiverKind, ReturnType, Signature, - Token, TraitItem, Type, + Error, Expr, File, FnArg, Ident, Item, LitBool, LitChar, Pat, ReceiverKind, ReturnType, + Signature, Token, TraitItem, Type, }; /// Override value types supported by the macro pub(crate) enum Override { Bool(LitBool), Char(LitChar), - None, + Expr(Expr), } /// Parsed input for the `derive_dialect!` macro @@ -80,20 +80,8 @@ impl Parse for DeriveDialectInput { Override::Bool(content.parse()?) } else if content.peek(LitChar) { Override::Char(content.parse()?) - } else if content.peek(Ident) { - let ident: Ident = content.parse()?; - if ident == "None" { - Override::None - } else { - return Err(Error::new( - ident.span(), - format!("Expected `true`, `false`, a char, or `None`, found `{ident}`"), - )); - } } else { - return Err( - content.error("Expected `true`, `false`, a char, or `None`") - ); + Override::Expr(content.parse()?) }; overrides.push((key, value)); if content.peek(Token![,]) { @@ -136,6 +124,7 @@ fn derive_dialect_inner(input: DeriveDialectInput) -> syn::Result { let methods = extract_dialect_methods(&file)?; // Validate overrides + let method_names: HashSet<_> = methods.iter().map(|m| m.name.to_string()).collect(); let bool_names: HashSet<_> = methods .iter() .filter(|m| is_bool_method(&m.signature)) @@ -143,6 +132,12 @@ fn derive_dialect_inner(input: DeriveDialectInput) -> syn::Result { .collect(); for (key, value) in &input.overrides { let key_str = key.to_string(); + if !method_names.contains(&key_str) { + return Err(Error::new( + key.span(), + format!("Unknown method `{key_str}`"), + )); + } match value { Override::Bool(_) if !bool_names.contains(&key_str) => { return Err(Error::new( @@ -150,10 +145,10 @@ fn derive_dialect_inner(input: DeriveDialectInput) -> syn::Result { format!("Unknown boolean method `{key_str}`"), )); } - Override::Char(_) | Override::None if key_str != "identifier_quote_style" => { + Override::Char(_) if key_str != "identifier_quote_style" => { return Err(Error::new( key.span(), - format!("Char/None only valid for `identifier_quote_style`, not `{key_str}`"), + format!("Char only valid for `identifier_quote_style`, not `{key_str}`"), )); } _ => {} @@ -214,10 +209,9 @@ fn generate_derived_dialect(input: &DeriveDialectInput, methods: &[DialectMethod fn identifier_quote_style(&self, _: &str) -> Option { Some(#c) } } } - Some(Override::None) => { - quote_spanned! { method_name.span() => - fn identifier_quote_style(&self, _: &str) -> Option { None } - } + Some(Override::Expr(expr)) => { + let sig = &method.signature; + quote_spanned! { method_name.span() => #sig { #expr } } } None => delegate(method), } @@ -230,7 +224,7 @@ fn generate_derived_dialect(input: &DeriveDialectInput, methods: &[DialectMethod use ::core::iter::Peekable; use ::core::str::Chars; use sqlparser::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement}; - use sqlparser::dialect::{Dialect, Precedence}; + use sqlparser::dialect::{BindPlaceholderStyle, Dialect, Precedence}; use sqlparser::keywords::Keyword; use sqlparser::parser::{Parser, ParserError}; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3ce71eb0be..9fb06d6048 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -109,6 +109,23 @@ pub use self::teradata::TeradataDialect; /// assert_eq!(d.identifier_quote_style("foo"), None); /// ``` /// +/// # Overriding other methods +/// +/// Any other method can be overridden with an expression of the method's return type: +/// ``` +/// use sqlparser::derive_dialect; +/// use sqlparser::dialect::{BindPlaceholderStyle, Dialect, GenericDialect}; +/// +/// derive_dialect!(DollarBindDialect, GenericDialect, +/// overrides = { ordered_bind_placeholder_style = Some(BindPlaceholderStyle::DollarNumbered) } +/// ); +/// let d: &dyn Dialect = &DollarBindDialect::new(); +/// assert_eq!( +/// d.ordered_bind_placeholder_style(), +/// Some(BindPlaceholderStyle::DollarNumbered) +/// ); +/// ``` +/// /// # Type Identity /// /// By default, derived dialects have their own `TypeId`. Set `preserve_type_id = true` to @@ -167,6 +184,17 @@ macro_rules! dialect_is { } } +/// Placeholder spelling for ordered unnamed bind parameters. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum BindPlaceholderStyle { + /// `?` + QuestionMark, + /// `$1`, `$2`, ... + DollarNumbered, +} + /// Encapsulates the differences between SQL implementations. /// /// # SQL Dialects @@ -254,6 +282,15 @@ pub trait Dialect: Debug + Any { None } + /// Return the placeholder syntax this server accepts for ordered unnamed bind parameters. + /// + /// This describes the server contract rather than every placeholder the parser can tokenize. + /// `None` means this dialect does not specify an ordered bind syntax. + /// See [`Dialect::supports_dollar_placeholder`] for SQLite-style named `$name` placeholders. + fn ordered_bind_placeholder_style(&self) -> Option { + None + } + /// Determine if a character is a valid start character for an unquoted identifier fn is_identifier_start(&self, ch: char) -> bool; @@ -1219,8 +1256,11 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if this dialect allows dollar placeholders - /// e.g. `SELECT $var` (SQLite) + /// Returns true if this dialect allows SQLite-style named dollar placeholders, + /// for example `SELECT $name`. + /// + /// This does not describe PostgreSQL-style ordered binds such as `SELECT $1`. + /// See [`Dialect::ordered_bind_placeholder_style`]. fn supports_dollar_placeholder(&self) -> bool { false } @@ -2232,6 +2272,7 @@ mod tests { supports_order_by_all = true, supports_nested_comments = true, supports_triple_quoted_string = true, + ordered_bind_placeholder_style = Some(BindPlaceholderStyle::DollarNumbered), }, ); let dialect = EnhancedGenericDialect::new(); @@ -2239,6 +2280,10 @@ mod tests { assert!(dialect.supports_order_by_all()); assert!(dialect.supports_nested_comments()); assert!(dialect.supports_triple_quoted_string()); + assert_eq!( + dialect.ordered_bind_placeholder_style(), + Some(BindPlaceholderStyle::DollarNumbered) + ); let d: &dyn Dialect = &dialect; assert!(d.is::()); @@ -2272,6 +2317,23 @@ mod tests { } } + #[test] + fn ordered_bind_placeholder_style() { + let tests: Vec<(&dyn Dialect, Option)> = vec![ + (&GenericDialect {}, None), + (&MySqlDialect {}, Some(BindPlaceholderStyle::QuestionMark)), + ( + &PostgreSqlDialect {}, + Some(BindPlaceholderStyle::DollarNumbered), + ), + (&SQLiteDialect {}, Some(BindPlaceholderStyle::QuestionMark)), + ]; + + for (dialect, expected) in tests { + assert_eq!(dialect.ordered_bind_placeholder_style(), expected); + } + } + #[test] fn parse_with_wrapped_dialect() { /// Wrapper for a dialect. In a real-world example, this wrapper @@ -2308,6 +2370,10 @@ mod tests { self.0.identifier_quote_style(identifier) } + fn ordered_bind_placeholder_style(&self) -> Option { + self.0.ordered_bind_placeholder_style() + } + fn supports_string_literal_backslash_escape(&self) -> bool { self.0.supports_string_literal_backslash_escape() } @@ -2374,6 +2440,10 @@ mod tests { let statement = r"SELECT 'Wayne\'s World'"; let res1 = Parser::parse_sql(&MySqlDialect {}, statement); let res2 = Parser::parse_sql(&WrappedDialect(MySqlDialect {}), statement); + assert_eq!( + WrappedDialect(MySqlDialect {}).ordered_bind_placeholder_style(), + Some(BindPlaceholderStyle::QuestionMark) + ); assert_eq!(res1.unwrap(), res2.unwrap()); } } diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 4c8b407f37..23c15a3a38 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -20,7 +20,7 @@ use alloc::boxed::Box; use crate::{ ast::{BinaryOperator, Expr, LockTable, LockTableType, Statement}, - dialect::{Dialect, Precedence}, + dialect::{BindPlaceholderStyle, Dialect, Precedence}, keywords::Keyword, parser::{Parser, ParserError}, tokenizer::Token, @@ -74,6 +74,10 @@ impl Dialect for MySqlDialect { Some('`') } + fn ordered_bind_placeholder_style(&self) -> Option { + Some(BindPlaceholderStyle::QuestionMark) + } + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences fn supports_string_literal_backslash_escape(&self) -> bool { true diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index c060f2a8c1..ce6826b46c 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -28,7 +28,7 @@ // limitations under the License. use log::debug; -use crate::dialect::{Dialect, Precedence}; +use crate::dialect::{BindPlaceholderStyle, Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; @@ -232,6 +232,10 @@ impl Dialect for PostgreSqlDialect { Some('"') } + fn ordered_bind_placeholder_style(&self) -> Option { + Some(BindPlaceholderStyle::DollarNumbered) + } + fn is_delimited_identifier_start(&self, ch: char) -> bool { ch == '"' // Postgres does not support backticks to quote identifiers } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 41b0e8be16..e7c5b523af 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -20,7 +20,7 @@ use alloc::boxed::Box; use crate::ast::BinaryOperator; use crate::ast::{Expr, Statement}; -use crate::dialect::{Dialect, Precedence}; +use crate::dialect::{BindPlaceholderStyle, Dialect, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; @@ -47,6 +47,10 @@ impl Dialect for SQLiteDialect { Some('`') } + fn ordered_bind_placeholder_style(&self) -> Option { + Some(BindPlaceholderStyle::QuestionMark) + } + fn is_identifier_start(&self, ch: char) -> bool { // See https://www.sqlite.org/draft/tokenreq.html ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' || ch >= '\u{0080}' From 0e22a9d5609c30f5dd2fddfb0bb33305b4b95015 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:55:55 +0200 Subject: [PATCH 2/2] Report Redshift numbered bind placeholders --- src/dialect/mod.rs | 12 ++++++------ src/dialect/redshift.rs | 6 +++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9fb06d6048..d848f8b32a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -286,7 +286,7 @@ pub trait Dialect: Debug + Any { /// /// This describes the server contract rather than every placeholder the parser can tokenize. /// `None` means this dialect does not specify an ordered bind syntax. - /// See [`Dialect::supports_dollar_placeholder`] for SQLite-style named `$name` placeholders. + /// Dollar-quoted strings are controlled by [`Dialect::supports_dollar_placeholder`]. fn ordered_bind_placeholder_style(&self) -> Option { None } @@ -1256,11 +1256,7 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if this dialect allows SQLite-style named dollar placeholders, - /// for example `SELECT $name`. - /// - /// This does not describe PostgreSQL-style ordered binds such as `SELECT $1`. - /// See [`Dialect::ordered_bind_placeholder_style`]. + /// Enables SQLite `$` placeholders containing `$` characters and disables dollar-quoted strings. fn supports_dollar_placeholder(&self) -> bool { false } @@ -2326,6 +2322,10 @@ mod tests { &PostgreSqlDialect {}, Some(BindPlaceholderStyle::DollarNumbered), ), + ( + &RedshiftSqlDialect {}, + Some(BindPlaceholderStyle::DollarNumbered), + ), (&SQLiteDialect {}, Some(BindPlaceholderStyle::QuestionMark)), ]; diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index f3dab66ffa..8204b23ff8 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::dialect::Dialect; +use crate::dialect::{BindPlaceholderStyle, Dialect}; use crate::keywords::Keyword; use core::iter::Peekable; use core::str::Chars; @@ -35,6 +35,10 @@ pub struct RedshiftSqlDialect {} // in the Postgres dialect, the query will be parsed as an array, while in the Redshift dialect it will // be a json path impl Dialect for RedshiftSqlDialect { + fn ordered_bind_placeholder_style(&self) -> Option { + Some(BindPlaceholderStyle::DollarNumbered) + } + /// Determine if a character starts a potential nested quoted identifier. /// Example: RedShift supports the following quote styles to all mean the same thing: /// ```sql