Skip to content

Commit 3f5fdeb

Browse files
Merge pull request #4 from AikidoSec/dollar
SQLite: Allow dollar signs in placeholder names
2 parents 4fdeb5c + f125b1f commit 3f5fdeb

File tree

10 files changed

+70
-20
lines changed

10 files changed

+70
-20
lines changed

src/ast/ddl.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -1123,15 +1123,18 @@ pub enum ColumnOption {
11231123
/// `DEFAULT <restricted-expr>`
11241124
Default(Expr),
11251125

1126-
/// ClickHouse supports `MATERIALIZE`, `EPHEMERAL` and `ALIAS` expr to generate default values.
1126+
/// `MATERIALIZE <expr>`
11271127
/// Syntax: `b INT MATERIALIZE (a + 1)`
1128+
///
11281129
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values)
1129-
1130-
/// `MATERIALIZE <expr>`
11311130
Materialized(Expr),
11321131
/// `EPHEMERAL [<expr>]`
1132+
///
1133+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values)
11331134
Ephemeral(Option<Expr>),
11341135
/// `ALIAS <expr>`
1136+
///
1137+
/// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/create/table#default_values)
11351138
Alias(Expr),
11361139

11371140
/// `{ PRIMARY KEY | UNIQUE } [<constraint_characteristics>]`
@@ -1330,7 +1333,7 @@ pub enum GeneratedExpressionMode {
13301333
#[must_use]
13311334
fn display_constraint_name(name: &'_ Option<Ident>) -> impl fmt::Display + '_ {
13321335
struct ConstraintName<'a>(&'a Option<Ident>);
1333-
impl<'a> fmt::Display for ConstraintName<'a> {
1336+
impl fmt::Display for ConstraintName<'_> {
13341337
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13351338
if let Some(name) = self.0 {
13361339
write!(f, "CONSTRAINT {name} ")?;
@@ -1351,7 +1354,7 @@ fn display_option<'a, T: fmt::Display>(
13511354
option: &'a Option<T>,
13521355
) -> impl fmt::Display + 'a {
13531356
struct OptionDisplay<'a, T>(&'a str, &'a str, &'a Option<T>);
1354-
impl<'a, T: fmt::Display> fmt::Display for OptionDisplay<'a, T> {
1357+
impl<T: fmt::Display> fmt::Display for OptionDisplay<'_, T> {
13551358
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13561359
if let Some(inner) = self.2 {
13571360
let (prefix, postfix) = (self.0, self.1);

src/ast/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ where
9999
sep: &'static str,
100100
}
101101

102-
impl<'a, T> fmt::Display for DisplaySeparated<'a, T>
102+
impl<T> fmt::Display for DisplaySeparated<'_, T>
103103
where
104104
T: fmt::Display,
105105
{

src/ast/query.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1597,7 +1597,7 @@ impl fmt::Display for Join {
15971597
}
15981598
fn suffix(constraint: &'_ JoinConstraint) -> impl fmt::Display + '_ {
15991599
struct Suffix<'a>(&'a JoinConstraint);
1600-
impl<'a> fmt::Display for Suffix<'a> {
1600+
impl fmt::Display for Suffix<'_> {
16011601
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
16021602
match self.0 {
16031603
JoinConstraint::On(expr) => write!(f, " ON {expr}"),

src/ast/value.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ pub struct EscapeQuotedString<'a> {
261261
quote: char,
262262
}
263263

264-
impl<'a> fmt::Display for EscapeQuotedString<'a> {
264+
impl fmt::Display for EscapeQuotedString<'_> {
265265
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
266266
// EscapeQuotedString doesn't know which mode of escape was
267267
// chosen by the user. So this code must to correctly display
@@ -325,7 +325,7 @@ pub fn escape_double_quote_string(s: &str) -> EscapeQuotedString<'_> {
325325

326326
pub struct EscapeEscapedStringLiteral<'a>(&'a str);
327327

328-
impl<'a> fmt::Display for EscapeEscapedStringLiteral<'a> {
328+
impl fmt::Display for EscapeEscapedStringLiteral<'_> {
329329
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
330330
for c in self.0.chars() {
331331
match c {
@@ -359,7 +359,7 @@ pub fn escape_escaped_string(s: &str) -> EscapeEscapedStringLiteral<'_> {
359359

360360
pub struct EscapeUnicodeStringLiteral<'a>(&'a str);
361361

362-
impl<'a> fmt::Display for EscapeUnicodeStringLiteral<'a> {
362+
impl fmt::Display for EscapeUnicodeStringLiteral<'_> {
363363
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
364364
for c in self.0.chars() {
365365
match c {

src/dialect/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,12 @@ pub trait Dialect: Debug + Any {
527527
false
528528
}
529529

530+
/// Returns true if this dialect allows dollar placeholders
531+
/// e.g. `SELECT $var` (SQLite)
532+
fn supports_dollar_placeholder(&self) -> bool {
533+
false
534+
}
535+
530536
/// Does the dialect support with clause in create index statement?
531537
/// e.g. `CREATE INDEX idx ON t WITH (key = value, key2)`
532538
fn supports_create_index_with_clause(&self) -> bool {

src/dialect/sqlite.rs

+4
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,8 @@ impl Dialect for SQLiteDialect {
8181
fn supports_asc_desc_in_column_definition(&self) -> bool {
8282
true
8383
}
84+
85+
fn supports_dollar_placeholder(&self) -> bool {
86+
true
87+
}
8488
}

src/parser/alter.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::{
2626
tokenizer::Token,
2727
};
2828

29-
impl<'a> Parser<'a> {
29+
impl Parser<'_> {
3030
pub fn parse_alter_role(&mut self) -> Result<Statement, ParserError> {
3131
if dialect_of!(self is PostgreSqlDialect) {
3232
return self.parse_pg_alter_role();

src/parser/mod.rs

+2-4
Original file line numberDiff line numberDiff line change
@@ -10458,13 +10458,12 @@ impl<'a> Parser<'a> {
1045810458
Ok(ExprWithAlias { expr, alias })
1045910459
}
1046010460
/// Parses an expression with an optional alias
10461-
10461+
///
1046210462
/// Examples:
10463-
10463+
///
1046410464
/// ```sql
1046510465
/// SUM(price) AS total_price
1046610466
/// ```
10467-
1046810467
/// ```sql
1046910468
/// SUM(price)
1047010469
/// ```
@@ -10480,7 +10479,6 @@ impl<'a> Parser<'a> {
1048010479
/// assert_eq!(Some("b".to_string()), expr_with_alias.alias.map(|x|x.value));
1048110480
/// # Ok(())
1048210481
/// # }
10483-
1048410482
pub fn parse_expr_with_alias(&mut self) -> Result<ExprWithAlias, ParserError> {
1048510483
let expr = self.parse_expr()?;
1048610484
let alias = if self.parse_keyword(Keyword::AS) {

src/tokenizer.rs

+34-5
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ struct State<'a> {
504504
pub col: u64,
505505
}
506506

507-
impl<'a> State<'a> {
507+
impl State<'_> {
508508
/// return the next character and advance the stream
509509
pub fn next(&mut self) -> Option<char> {
510510
match self.peekable.next() {
@@ -1278,7 +1278,8 @@ impl<'a> Tokenizer<'a> {
12781278

12791279
chars.next();
12801280

1281-
if let Some('$') = chars.peek() {
1281+
// If the dialect does not support dollar-quoted strings, then `$$` is rather a placeholder.
1282+
if matches!(chars.peek(), Some('$')) && !self.dialect.supports_dollar_placeholder() {
12821283
chars.next();
12831284

12841285
let mut is_terminated = false;
@@ -1312,10 +1313,14 @@ impl<'a> Tokenizer<'a> {
13121313
};
13131314
} else {
13141315
value.push_str(&peeking_take_while(chars, |ch| {
1315-
ch.is_alphanumeric() || ch == '_'
1316+
ch.is_alphanumeric()
1317+
|| ch == '_'
1318+
// Allow $ as a placeholder character if the dialect supports it
1319+
|| matches!(ch, '$' if self.dialect.supports_dollar_placeholder())
13161320
}));
13171321

1318-
if let Some('$') = chars.peek() {
1322+
// If the dialect does not support dollar-quoted strings, don't look for the end delimiter.
1323+
if matches!(chars.peek(), Some('$')) && !self.dialect.supports_dollar_placeholder() {
13191324
chars.next();
13201325

13211326
'searching_for_end: loop {
@@ -1885,7 +1890,7 @@ fn take_char_from_hex_digits(
18851890
mod tests {
18861891
use super::*;
18871892
use crate::dialect::{
1888-
BigQueryDialect, ClickHouseDialect, HiveDialect, MsSqlDialect, MySqlDialect,
1893+
BigQueryDialect, ClickHouseDialect, HiveDialect, MsSqlDialect, MySqlDialect, SQLiteDialect,
18891894
};
18901895
use core::fmt::Debug;
18911896

@@ -2321,6 +2326,30 @@ mod tests {
23212326
);
23222327
}
23232328

2329+
#[test]
2330+
fn tokenize_dollar_placeholder() {
2331+
let sql = String::from("SELECT $$, $$ABC$$, $ABC$, $ABC");
2332+
let dialect = SQLiteDialect {};
2333+
let tokens = Tokenizer::new(&dialect, &sql).tokenize().unwrap();
2334+
assert_eq!(
2335+
tokens,
2336+
vec![
2337+
Token::make_keyword("SELECT"),
2338+
Token::Whitespace(Whitespace::Space),
2339+
Token::Placeholder("$$".into()),
2340+
Token::Comma,
2341+
Token::Whitespace(Whitespace::Space),
2342+
Token::Placeholder("$$ABC$$".into()),
2343+
Token::Comma,
2344+
Token::Whitespace(Whitespace::Space),
2345+
Token::Placeholder("$ABC$".into()),
2346+
Token::Comma,
2347+
Token::Whitespace(Whitespace::Space),
2348+
Token::Placeholder("$ABC".into()),
2349+
]
2350+
);
2351+
}
2352+
23242353
#[test]
23252354
fn tokenize_dollar_quoted_string_untagged() {
23262355
let sql =

tests/sqlparser_sqlite.rs

+10
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,16 @@ fn test_dollar_identifier_as_placeholder() {
568568
}
569569
_ => unreachable!(),
570570
}
571+
572+
// $$ is a valid placeholder in SQLite
573+
match sqlite().verified_expr("id = $$") {
574+
Expr::BinaryOp { op, left, right } => {
575+
assert_eq!(op, BinaryOperator::Eq);
576+
assert_eq!(left, Box::new(Expr::Identifier(Ident::new("id"))));
577+
assert_eq!(right, Box::new(Expr::Value(Placeholder("$$".to_string()))));
578+
}
579+
_ => unreachable!(),
580+
}
571581
}
572582

573583
fn sqlite() -> TestedDialects {

0 commit comments

Comments
 (0)