Skip to content

Commit 1f82e56

Browse files
Add ordered bind placeholder dialect metadata
1 parent dc8f0c4 commit 1f82e56

5 files changed

Lines changed: 87 additions & 28 deletions

File tree

‎derive/src/dialect.rs‎

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ use std::collections::HashSet;
2323
use syn::{
2424
braced,
2525
parse::{Parse, ParseStream},
26-
Error, File, FnArg, Ident, Item, LitBool, LitChar, Pat, ReceiverKind, ReturnType, Signature,
27-
Token, TraitItem, Type,
26+
Error, Expr, File, FnArg, Ident, Item, LitBool, LitChar, Pat, ReceiverKind, ReturnType,
27+
Signature, Token, TraitItem, Type,
2828
};
2929

3030
/// Override value types supported by the macro
3131
pub(crate) enum Override {
3232
Bool(LitBool),
3333
Char(LitChar),
34-
None,
34+
Expr(Expr),
3535
}
3636

3737
/// Parsed input for the `derive_dialect!` macro
@@ -80,20 +80,8 @@ impl Parse for DeriveDialectInput {
8080
Override::Bool(content.parse()?)
8181
} else if content.peek(LitChar) {
8282
Override::Char(content.parse()?)
83-
} else if content.peek(Ident) {
84-
let ident: Ident = content.parse()?;
85-
if ident == "None" {
86-
Override::None
87-
} else {
88-
return Err(Error::new(
89-
ident.span(),
90-
format!("Expected `true`, `false`, a char, or `None`, found `{ident}`"),
91-
));
92-
}
9383
} else {
94-
return Err(
95-
content.error("Expected `true`, `false`, a char, or `None`")
96-
);
84+
Override::Expr(content.parse()?)
9785
};
9886
overrides.push((key, value));
9987
if content.peek(Token![,]) {
@@ -136,24 +124,31 @@ fn derive_dialect_inner(input: DeriveDialectInput) -> syn::Result<TokenStream> {
136124
let methods = extract_dialect_methods(&file)?;
137125

138126
// Validate overrides
127+
let method_names: HashSet<_> = methods.iter().map(|m| m.name.to_string()).collect();
139128
let bool_names: HashSet<_> = methods
140129
.iter()
141130
.filter(|m| is_bool_method(&m.signature))
142131
.map(|m| m.name.to_string())
143132
.collect();
144133
for (key, value) in &input.overrides {
145134
let key_str = key.to_string();
135+
if !method_names.contains(&key_str) {
136+
return Err(Error::new(
137+
key.span(),
138+
format!("Unknown method `{key_str}`"),
139+
));
140+
}
146141
match value {
147142
Override::Bool(_) if !bool_names.contains(&key_str) => {
148143
return Err(Error::new(
149144
key.span(),
150145
format!("Unknown boolean method `{key_str}`"),
151146
));
152147
}
153-
Override::Char(_) | Override::None if key_str != "identifier_quote_style" => {
148+
Override::Char(_) if key_str != "identifier_quote_style" => {
154149
return Err(Error::new(
155150
key.span(),
156-
format!("Char/None only valid for `identifier_quote_style`, not `{key_str}`"),
151+
format!("Char only valid for `identifier_quote_style`, not `{key_str}`"),
157152
));
158153
}
159154
_ => {}
@@ -214,10 +209,9 @@ fn generate_derived_dialect(input: &DeriveDialectInput, methods: &[DialectMethod
214209
fn identifier_quote_style(&self, _: &str) -> Option<char> { Some(#c) }
215210
}
216211
}
217-
Some(Override::None) => {
218-
quote_spanned! { method_name.span() =>
219-
fn identifier_quote_style(&self, _: &str) -> Option<char> { None }
220-
}
212+
Some(Override::Expr(expr)) => {
213+
let sig = &method.signature;
214+
quote_spanned! { method_name.span() => #sig { #expr } }
221215
}
222216
None => delegate(method),
223217
}
@@ -230,7 +224,7 @@ fn generate_derived_dialect(input: &DeriveDialectInput, methods: &[DialectMethod
230224
use ::core::iter::Peekable;
231225
use ::core::str::Chars;
232226
use sqlparser::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement};
233-
use sqlparser::dialect::{Dialect, Precedence};
227+
use sqlparser::dialect::{BindPlaceholderStyle, Dialect, Precedence};
234228
use sqlparser::keywords::Keyword;
235229
use sqlparser::parser::{Parser, ParserError};
236230

‎src/dialect/mod.rs‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ macro_rules! dialect_is {
167167
}
168168
}
169169

170+
/// Placeholder spelling for ordered unnamed bind parameters.
171+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
172+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173+
#[non_exhaustive]
174+
pub enum BindPlaceholderStyle {
175+
/// `?`
176+
QuestionMark,
177+
/// `$1`, `$2`, ...
178+
DollarNumbered,
179+
}
180+
170181
/// Encapsulates the differences between SQL implementations.
171182
///
172183
/// # SQL Dialects
@@ -254,6 +265,15 @@ pub trait Dialect: Debug + Any {
254265
None
255266
}
256267

268+
/// Return the placeholder syntax this server accepts for ordered unnamed bind parameters.
269+
///
270+
/// This describes the server contract rather than every placeholder the parser can tokenize.
271+
/// `None` means this dialect does not specify an ordered bind syntax.
272+
/// See [`Dialect::supports_dollar_placeholder`] for SQLite-style named `$name` placeholders.
273+
fn ordered_bind_placeholder_style(&self) -> Option<BindPlaceholderStyle> {
274+
None
275+
}
276+
257277
/// Determine if a character is a valid start character for an unquoted identifier
258278
fn is_identifier_start(&self, ch: char) -> bool;
259279

@@ -1112,8 +1132,11 @@ pub trait Dialect: Debug + Any {
11121132
false
11131133
}
11141134

1115-
/// Returns true if this dialect allows dollar placeholders
1116-
/// e.g. `SELECT $var` (SQLite)
1135+
/// Returns true if this dialect allows SQLite-style named dollar placeholders,
1136+
/// for example `SELECT $name`.
1137+
///
1138+
/// This does not describe PostgreSQL-style ordered binds such as `SELECT $1`.
1139+
/// See [`Dialect::ordered_bind_placeholder_style`].
11171140
fn supports_dollar_placeholder(&self) -> bool {
11181141
false
11191142
}
@@ -2001,13 +2024,18 @@ mod tests {
20012024
supports_order_by_all = true,
20022025
supports_nested_comments = true,
20032026
supports_triple_quoted_string = true,
2027+
ordered_bind_placeholder_style = Some(BindPlaceholderStyle::DollarNumbered),
20042028
},
20052029
);
20062030
let dialect = EnhancedGenericDialect::new();
20072031

20082032
assert!(dialect.supports_order_by_all());
20092033
assert!(dialect.supports_nested_comments());
20102034
assert!(dialect.supports_triple_quoted_string());
2035+
assert_eq!(
2036+
dialect.ordered_bind_placeholder_style(),
2037+
Some(BindPlaceholderStyle::DollarNumbered)
2038+
);
20112039

20122040
let d: &dyn Dialect = &dialect;
20132041
assert!(d.is::<GenericDialect>());
@@ -2041,6 +2069,23 @@ mod tests {
20412069
}
20422070
}
20432071

2072+
#[test]
2073+
fn ordered_bind_placeholder_style() {
2074+
let tests: Vec<(&dyn Dialect, Option<BindPlaceholderStyle>)> = vec![
2075+
(&GenericDialect {}, None),
2076+
(&MySqlDialect {}, Some(BindPlaceholderStyle::QuestionMark)),
2077+
(
2078+
&PostgreSqlDialect {},
2079+
Some(BindPlaceholderStyle::DollarNumbered),
2080+
),
2081+
(&SQLiteDialect {}, Some(BindPlaceholderStyle::QuestionMark)),
2082+
];
2083+
2084+
for (dialect, expected) in tests {
2085+
assert_eq!(dialect.ordered_bind_placeholder_style(), expected);
2086+
}
2087+
}
2088+
20442089
#[test]
20452090
fn parse_with_wrapped_dialect() {
20462091
/// Wrapper for a dialect. In a real-world example, this wrapper
@@ -2077,6 +2122,10 @@ mod tests {
20772122
self.0.identifier_quote_style(identifier)
20782123
}
20792124

2125+
fn ordered_bind_placeholder_style(&self) -> Option<BindPlaceholderStyle> {
2126+
self.0.ordered_bind_placeholder_style()
2127+
}
2128+
20802129
fn supports_string_literal_backslash_escape(&self) -> bool {
20812130
self.0.supports_string_literal_backslash_escape()
20822131
}
@@ -2144,6 +2193,10 @@ mod tests {
21442193
let statement = r#"SELECT 'Wayne\'s World'"#;
21452194
let res1 = Parser::parse_sql(&MySqlDialect {}, statement);
21462195
let res2 = Parser::parse_sql(&WrappedDialect(MySqlDialect {}), statement);
2196+
assert_eq!(
2197+
WrappedDialect(MySqlDialect {}).ordered_bind_placeholder_style(),
2198+
Some(BindPlaceholderStyle::QuestionMark)
2199+
);
21472200
assert!(res1.is_ok());
21482201
assert_eq!(res1, res2);
21492202
}

‎src/dialect/mysql.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use alloc::boxed::Box;
2020

2121
use crate::{
2222
ast::{BinaryOperator, Expr, LockTable, LockTableType, Statement},
23-
dialect::Dialect,
23+
dialect::{BindPlaceholderStyle, Dialect},
2424
keywords::Keyword,
2525
parser::{Parser, ParserError},
2626
};
@@ -67,6 +67,10 @@ impl Dialect for MySqlDialect {
6767
Some('`')
6868
}
6969

70+
fn ordered_bind_placeholder_style(&self) -> Option<BindPlaceholderStyle> {
71+
Some(BindPlaceholderStyle::QuestionMark)
72+
}
73+
7074
// See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences
7175
fn supports_string_literal_backslash_escape(&self) -> bool {
7276
true

‎src/dialect/postgresql.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
// limitations under the License.
2929
use log::debug;
3030

31-
use crate::dialect::{Dialect, Precedence};
31+
use crate::dialect::{BindPlaceholderStyle, Dialect, Precedence};
3232
use crate::keywords::Keyword;
3333
use crate::parser::{Parser, ParserError};
3434
use crate::tokenizer::Token;
@@ -77,6 +77,10 @@ impl Dialect for PostgreSqlDialect {
7777
Some('"')
7878
}
7979

80+
fn ordered_bind_placeholder_style(&self) -> Option<BindPlaceholderStyle> {
81+
Some(BindPlaceholderStyle::DollarNumbered)
82+
}
83+
8084
fn is_delimited_identifier_start(&self, ch: char) -> bool {
8185
ch == '"' // Postgres does not support backticks to quote identifiers
8286
}

‎src/dialect/sqlite.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use alloc::boxed::Box;
2020

2121
use crate::ast::BinaryOperator;
2222
use crate::ast::{Expr, Statement};
23-
use crate::dialect::Dialect;
23+
use crate::dialect::{BindPlaceholderStyle, Dialect};
2424
use crate::keywords::Keyword;
2525
use crate::parser::{Parser, ParserError};
2626

@@ -46,6 +46,10 @@ impl Dialect for SQLiteDialect {
4646
Some('`')
4747
}
4848

49+
fn ordered_bind_placeholder_style(&self) -> Option<BindPlaceholderStyle> {
50+
Some(BindPlaceholderStyle::QuestionMark)
51+
}
52+
4953
fn is_identifier_start(&self, ch: char) -> bool {
5054
// See https://www.sqlite.org/draft/tokenreq.html
5155
ch.is_ascii_lowercase()

0 commit comments

Comments
 (0)