Skip to content

Commit 6d8a068

Browse files
Preserve single quote literal display
1 parent 75a1040 commit 6d8a068

3 files changed

Lines changed: 32 additions & 29 deletions

File tree

src/ast/value.rs

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -533,28 +533,11 @@ impl fmt::Display for NormalizationForm {
533533
pub struct EscapeQuotedString<'a> {
534534
string: &'a str,
535535
quote: char,
536+
always_escape_quote: bool,
536537
}
537538

538539
impl fmt::Display for EscapeQuotedString<'_> {
539540
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
540-
// EscapeQuotedString doesn't know which mode of escape was
541-
// chosen by the user. So this code must to correctly display
542-
// strings without knowing if the strings are already escaped
543-
// or not.
544-
//
545-
// If the quote symbol in the string is repeated twice, OR, if
546-
// the quote symbol is after backslash, display all the chars
547-
// without any escape. However, if the quote symbol is used
548-
// just between usual chars, `fmt()` should display it twice."
549-
//
550-
// The following table has examples
551-
//
552-
// | original query | mode | AST Node | serialized |
553-
// | ------------- | --------- | -------------------------------------------------- | ------------ |
554-
// | `"A""B""A"` | no-escape | `DoubleQuotedString(String::from("A\"\"B\"\"A"))` | `"A""B""A"` |
555-
// | `"A""B""A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` |
556-
// | `"A\"B\"A"` | no-escape | `DoubleQuotedString(String::from("A\\\"B\\\"A"))` | `"A\"B\"A"` |
557-
// | `"A\"B\"A"` | default | `DoubleQuotedString(String::from("A\"B\"A"))` | `"A""B""A"` |
558541
let quote = self.quote;
559542
let mut previous_char = char::default();
560543
let mut start_idx = 0;
@@ -563,20 +546,15 @@ impl fmt::Display for EscapeQuotedString<'_> {
563546
match ch {
564547
char if char == quote => {
565548
if previous_char == '\\' {
566-
// the quote is already escaped with a backslash, skip
567549
peekable_chars.next();
568550
continue;
569551
}
570552
peekable_chars.next();
571553
match peekable_chars.peek() {
572-
Some((_, c)) if *c == quote => {
573-
// the quote is already escaped with another quote, skip
554+
Some((_, c)) if !self.always_escape_quote && *c == quote => {
574555
peekable_chars.next();
575556
}
576557
_ => {
577-
// The quote is not escaped.
578-
// Including idx in the range, so the quote at idx will be printed twice:
579-
// in this call to write_str() and in the next one.
580558
let end_idx = idx + ch.len_utf8();
581559
f.write_str(&self.string[start_idx..end_idx])?;
582560
start_idx = idx;
@@ -597,12 +575,20 @@ impl fmt::Display for EscapeQuotedString<'_> {
597575
/// Return a helper which formats `string` for inclusion inside a quoted
598576
/// literal that uses `quote` as the delimiter.
599577
pub fn escape_quoted_string(string: &str, quote: char) -> EscapeQuotedString<'_> {
600-
EscapeQuotedString { string, quote }
578+
EscapeQuotedString {
579+
string,
580+
quote,
581+
always_escape_quote: false,
582+
}
601583
}
602584

603585
/// Convenience wrapper for escaping strings for single-quoted literals (`'`).
604586
pub fn escape_single_quote_string(s: &str) -> EscapeQuotedString<'_> {
605-
escape_quoted_string(s, '\'')
587+
EscapeQuotedString {
588+
string: s,
589+
quote: '\'',
590+
always_escape_quote: true,
591+
}
606592
}
607593

608594
/// Convenience wrapper for escaping strings for double-quoted literals (`").`

tests/sqlparser_common.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,13 +1585,21 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() {
15851585
let sql = "SELECT id, fname, lname FROM customer \
15861586
WHERE salary <> 'Jim''s salary'";
15871587

1588-
let ast = TestedDialects::new_with_options(
1588+
let statements = TestedDialects::new_with_options(
15891589
vec![Box::new(MySqlDialect {})],
15901590
ParserOptions::new()
15911591
.with_trailing_commas(true)
15921592
.with_unescape(false),
15931593
)
1594-
.verified_only_select(sql);
1594+
.parse_sql_statements(sql)
1595+
.unwrap();
1596+
let Statement::Query(query) = only(statements) else {
1597+
unreachable!()
1598+
};
1599+
let SetExpr::Select(ast) = *query.body else {
1600+
unreachable!()
1601+
};
1602+
let ast = *ast;
15951603

15961604
assert_eq!(
15971605
Some(Expr::BinaryOp {
@@ -1605,6 +1613,15 @@ fn parse_escaped_single_quote_string_predicate_with_no_escape() {
16051613
);
16061614
}
16071615

1616+
#[test]
1617+
fn parse_adjacent_single_quotes_round_trip() {
1618+
TestedDialects::new(vec![
1619+
Box::new(PostgreSqlDialect {}),
1620+
Box::new(MySqlDialect {}),
1621+
])
1622+
.verified_stmt("SELECT * FROM t WHERE ''''''");
1623+
}
1624+
16081625
#[test]
16091626
fn parse_number() {
16101627
let expr = verified_expr("1.0");

tests/sqlparser_mysql.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1679,7 +1679,7 @@ fn check_roundtrip_of_escaped_string() {
16791679
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
16801680
.verified_stmt(r"SELECT 'I\'m fine'");
16811681
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
1682-
.verified_stmt(r#"SELECT 'I''m fine'"#);
1682+
.one_statement_parses_to(r#"SELECT 'I''m fine'"#, "");
16831683
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())
16841684
.verified_stmt(r"SELECT 'I\\\'m fine'");
16851685
TestedDialects::new_with_options(vec![Box::new(MySqlDialect {})], options.clone())

0 commit comments

Comments
 (0)