Skip to content

Commit ea0eb1b

Browse files
authored
fix: mysql backslash escaping (apache#373)
* fix: mysql backslash escaping * fixes
1 parent 60ad78d commit ea0eb1b

File tree

5 files changed

+67
-6
lines changed

5 files changed

+67
-6
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ Cargo.lock
1212

1313
# IDEs
1414
.idea
15+
.vscode

src/ast/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
// limitations under the License.
1212

1313
//! SQL Abstract Syntax Tree (AST) types
14-
1514
mod data_type;
1615
mod ddl;
1716
mod operator;
@@ -1874,6 +1873,7 @@ pub enum HiveRowFormat {
18741873
DELIMITED,
18751874
}
18761875

1876+
#[allow(clippy::large_enum_variant)]
18771877
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18781878
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18791879
#[allow(clippy::large_enum_variant)]

src/dialect/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub use self::sqlite::SQLiteDialect;
3535
pub use crate::keywords;
3636

3737
/// `dialect_of!(parser is SQLiteDialect | GenericDialect)` evaluates
38-
/// to `true` iff `parser.dialect` is one of the `Dialect`s specified.
38+
/// to `true` if `parser.dialect` is one of the `Dialect`s specified.
3939
macro_rules! dialect_of {
4040
( $parsed_dialect: ident is $($dialect_type: ty)|+ ) => {
4141
($($parsed_dialect.dialect.is::<$dialect_type>())||+)

src/tokenizer.rs

+18-4
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ use core::str::Chars;
3131
#[cfg(feature = "serde")]
3232
use serde::{Deserialize, Serialize};
3333

34-
use crate::dialect::Dialect;
3534
use crate::dialect::SnowflakeDialect;
35+
use crate::dialect::{Dialect, MySqlDialect};
3636
use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX};
3737

3838
/// SQL Token enumeration
@@ -411,6 +411,7 @@ impl<'a> Tokenizer<'a> {
411411
// string
412412
'\'' => {
413413
let s = self.tokenize_single_quoted_string(chars)?;
414+
414415
Ok(Some(Token::SingleQuotedString(s)))
415416
}
416417
// delimited (quoted) identifier
@@ -636,18 +637,31 @@ impl<'a> Tokenizer<'a> {
636637
) -> Result<String, TokenizerError> {
637638
let mut s = String::new();
638639
chars.next(); // consume the opening quote
640+
641+
// slash escaping is specific to MySQL dialect
642+
let mut is_escaped = false;
639643
while let Some(&ch) = chars.peek() {
640644
match ch {
641645
'\'' => {
642646
chars.next(); // consume
643-
let escaped_quote = chars.peek().map(|c| *c == '\'').unwrap_or(false);
644-
if escaped_quote {
645-
s.push('\'');
647+
if is_escaped {
648+
s.push(ch);
649+
is_escaped = false;
650+
} else if chars.peek().map(|c| *c == '\'').unwrap_or(false) {
651+
s.push(ch);
646652
chars.next();
647653
} else {
648654
return Ok(s);
649655
}
650656
}
657+
'\\' => {
658+
if dialect_of!(self is MySqlDialect) {
659+
is_escaped = !is_escaped;
660+
} else {
661+
s.push(ch);
662+
}
663+
chars.next();
664+
}
651665
_ => {
652666
chars.next(); // consume
653667
s.push(ch);

tests/sqlparser_mysql.rs

+46
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ mod test_utils;
1919

2020
use test_utils::*;
2121

22+
use sqlparser::ast::Expr;
23+
use sqlparser::ast::Value;
2224
use sqlparser::ast::*;
2325
use sqlparser::dialect::{GenericDialect, MySqlDialect};
2426
use sqlparser::tokenizer::Token;
@@ -176,6 +178,50 @@ fn parse_quote_identifiers() {
176178
}
177179
}
178180

181+
#[test]
182+
fn parse_unterminated_escape() {
183+
let sql = r#"SELECT 'I\'m not fine\'"#;
184+
let result = std::panic::catch_unwind(|| mysql().one_statement_parses_to(sql, ""));
185+
assert!(result.is_err());
186+
187+
let sql = r#"SELECT 'I\\'m not fine'"#;
188+
let result = std::panic::catch_unwind(|| mysql().one_statement_parses_to(sql, ""));
189+
assert!(result.is_err());
190+
}
191+
192+
#[test]
193+
fn parse_escaped_string() {
194+
let sql = r#"SELECT 'I\'m fine'"#;
195+
196+
let stmt = mysql().one_statement_parses_to(sql, "");
197+
198+
match stmt {
199+
Statement::Query(query) => match query.body {
200+
SetExpr::Select(value) => {
201+
let expr = expr_from_projection(only(&value.projection));
202+
assert_eq!(
203+
*expr,
204+
Expr::Value(Value::SingleQuotedString("I'm fine".to_string()))
205+
);
206+
}
207+
_ => unreachable!(),
208+
},
209+
_ => unreachable!(),
210+
};
211+
212+
let sql = r#"SELECT 'I''m fine'"#;
213+
214+
let projection = mysql().verified_only_select(sql).projection;
215+
let item = projection.get(0).unwrap();
216+
217+
match &item {
218+
SelectItem::UnnamedExpr(Expr::Value(value)) => {
219+
assert_eq!(*value, Value::SingleQuotedString("I'm fine".to_string()));
220+
}
221+
_ => unreachable!(),
222+
}
223+
}
224+
179225
#[test]
180226
fn parse_create_table_with_minimum_display_width() {
181227
let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_int INT(11), bar_bigint BIGINT(20))";

0 commit comments

Comments
 (0)