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
38 changes: 12 additions & 26 deletions src/ast/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,28 +533,11 @@ impl fmt::Display for NormalizationForm {
pub struct EscapeQuotedString<'a> {
string: &'a str,
quote: char,
always_escape_quote: bool,
}

impl fmt::Display for EscapeQuotedString<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// EscapeQuotedString doesn't know which mode of escape was
// chosen by the user. So this code must to correctly display
// strings without knowing if the strings are already escaped
// or not.
//
// If the quote symbol in the string is repeated twice, OR, if
// the quote symbol is after backslash, display all the chars
// without any escape. However, if the quote symbol is used
// just between usual chars, `fmt()` should display it twice."
//
// The following table has examples
//
// | original query | mode | AST Node | serialized |
// | ------------- | --------- | -------------------------------------------------- | ------------ |
// | `"A""B""A"` | no-escape | `DoubleQuotedString(String::from("A\"\"B\"\"A"))` | `"A""B""A"` |
// | `"A""B""A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` |
// | `"A\"B\"A"` | no-escape | `DoubleQuotedString(String::from("A\\\"B\\\"A"))` | `"A\"B\"A"` |
// | `"A\"B\"A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` |
let quote = self.quote;
let mut previous_char = char::default();
let mut start_idx = 0;
Expand All @@ -563,20 +546,15 @@ impl fmt::Display for EscapeQuotedString<'_> {
match ch {
char if char == quote => {
if previous_char == '\\' {
// the quote is already escaped with a backslash, skip
peekable_chars.next();
continue;
}
peekable_chars.next();
match peekable_chars.peek() {
Some((_, c)) if *c == quote => {
// the quote is already escaped with another quote, skip
Some((_, c)) if !self.always_escape_quote && *c == quote => {
peekable_chars.next();
}
_ => {
// The quote is not escaped.
// Including idx in the range, so the quote at idx will be printed twice:
// in this call to write_str() and in the next one.
let end_idx = idx + ch.len_utf8();
f.write_str(&self.string[start_idx..end_idx])?;
start_idx = idx;
Expand All @@ -597,12 +575,20 @@ impl fmt::Display for EscapeQuotedString<'_> {
/// Return a helper which formats `string` for inclusion inside a quoted
/// literal that uses `quote` as the delimiter.
pub fn escape_quoted_string(string: &str, quote: char) -> EscapeQuotedString<'_> {
EscapeQuotedString { string, quote }
EscapeQuotedString {
string,
quote,
always_escape_quote: false,
}
}

/// Convenience wrapper for escaping strings for single-quoted literals (`'`).
pub fn escape_single_quote_string(s: &str) -> EscapeQuotedString<'_> {
escape_quoted_string(s, '\'')
EscapeQuotedString {
string: s,
quote: '\'',
always_escape_quote: true,
}
}

/// Convenience wrapper for escaping strings for double-quoted literals (`").`
Expand Down
21 changes: 19 additions & 2 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1585,13 +1585,21 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() {
let sql = "SELECT id, fname, lname FROM customer \
WHERE salary <> 'Jim''s salary'";

let ast = TestedDialects::new_with_options(
let statements = TestedDialects::new_with_options(
vec![Box::new(MySqlDialect {})],
ParserOptions::new()
.with_trailing_commas(true)
.with_unescape(false),
)
.verified_only_select(sql);
.parse_sql_statements(sql)
.unwrap();
let Statement::Query(query) = only(statements) else {
unreachable!()
};
let SetExpr::Select(ast) = *query.body else {
unreachable!()
};
let ast = *ast;

assert_eq!(
Some(Expr::BinaryOp {
Expand All @@ -1605,6 +1613,15 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() {
);
}

#[test]
fn parse_adjacent_single_quotes_round_trip() {
TestedDialects::new(vec![
Box::new(PostgreSqlDialect {}),
Box::new(MySqlDialect {}),
])
.verified_stmt("SELECT * FROM t WHERE ''''''");
}

#[test]
fn parse_number() {
let expr = verified_expr("1.0");
Expand Down
2 changes: 1 addition & 1 deletion tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1679,7 +1679,7 @@ fn check_roundtrip_of_escaped_string() {
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
.verified_stmt(r"SELECT 'I\'m fine'");
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
.verified_stmt(r#"SELECT 'I''m fine'"#);
.one_statement_parses_to(r#"SELECT 'I''m fine'"#, "");
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
.verified_stmt(r"SELECT 'I\\\'m fine'");
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
Expand Down
Loading