Skip to content

Commit d19c6c3

Browse files
author
Riccardo Azzolini
authored
Fix escaping of trailing quote in quoted identifiers (#505)
* Generalize EscapeSingleQuoteString to arbitrary quote character * Fix escaping of trailing quote in quoted identifiers * Add new tests instead of modifying existing tests
1 parent cc2559c commit d19c6c3

File tree

4 files changed

+56
-18
lines changed

4 files changed

+56
-18
lines changed

src/ast/mod.rs

+3-11
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use alloc::{
2323
string::{String, ToString},
2424
vec::Vec,
2525
};
26-
use core::fmt::{self, Write};
26+
use core::fmt;
2727

2828
#[cfg(feature = "serde")]
2929
use serde::{Deserialize, Serialize};
@@ -128,16 +128,8 @@ impl fmt::Display for Ident {
128128
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129129
match self.quote_style {
130130
Some(q) if q == '"' || q == '\'' || q == '`' => {
131-
f.write_char(q)?;
132-
let mut first = true;
133-
for s in self.value.split_inclusive(q) {
134-
if !first {
135-
f.write_char(q)?;
136-
}
137-
first = false;
138-
f.write_str(s)?;
139-
}
140-
f.write_char(q)
131+
let escaped = value::escape_quoted_string(&self.value, q);
132+
write!(f, "{}{}{}", q, escaped, q)
141133
}
142134
Some(q) if q == '[' => write!(f, "[{}]", self.value),
143135
None => f.write_str(&self.value),

src/ast/value.rs

+14-7
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,16 @@ impl fmt::Display for DateTimeField {
178178
}
179179
}
180180

181-
pub struct EscapeSingleQuoteString<'a>(&'a str);
181+
pub struct EscapeQuotedString<'a> {
182+
string: &'a str,
183+
quote: char,
184+
}
182185

183-
impl<'a> fmt::Display for EscapeSingleQuoteString<'a> {
186+
impl<'a> fmt::Display for EscapeQuotedString<'a> {
184187
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
185-
for c in self.0.chars() {
186-
if c == '\'' {
187-
write!(f, "\'\'")?;
188+
for c in self.string.chars() {
189+
if c == self.quote {
190+
write!(f, "{q}{q}", q = self.quote)?;
188191
} else {
189192
write!(f, "{}", c)?;
190193
}
@@ -193,8 +196,12 @@ impl<'a> fmt::Display for EscapeSingleQuoteString<'a> {
193196
}
194197
}
195198

196-
pub fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> {
197-
EscapeSingleQuoteString(s)
199+
pub fn escape_quoted_string(string: &str, quote: char) -> EscapeQuotedString<'_> {
200+
EscapeQuotedString { string, quote }
201+
}
202+
203+
pub fn escape_single_quote_string(s: &str) -> EscapeQuotedString<'_> {
204+
escape_quoted_string(s, '\'')
198205
}
199206

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

tests/sqlparser_mysql.rs

+34
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,40 @@ fn parse_quote_identifiers_2() {
325325
);
326326
}
327327

328+
#[test]
329+
fn parse_quote_identifiers_3() {
330+
let sql = "SELECT ```quoted identifier```";
331+
assert_eq!(
332+
mysql().verified_stmt(sql),
333+
Statement::Query(Box::new(Query {
334+
with: None,
335+
body: SetExpr::Select(Box::new(Select {
336+
distinct: false,
337+
top: None,
338+
projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident {
339+
value: "`quoted identifier`".into(),
340+
quote_style: Some('`'),
341+
}))],
342+
into: None,
343+
from: vec![],
344+
lateral_views: vec![],
345+
selection: None,
346+
group_by: vec![],
347+
cluster_by: vec![],
348+
distribute_by: vec![],
349+
sort_by: vec![],
350+
having: None,
351+
qualify: None
352+
})),
353+
order_by: vec![],
354+
limit: None,
355+
offset: None,
356+
fetch: None,
357+
lock: None,
358+
}))
359+
);
360+
}
361+
328362
#[test]
329363
fn parse_unterminated_escape() {
330364
let sql = r#"SELECT 'I\'m not fine\'"#;

tests/sqlparser_postgres.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,11 @@ fn parse_quoted_identifier() {
14411441
pg_and_generic().verified_stmt(r#"SELECT "quoted "" ident""#);
14421442
}
14431443

1444+
#[test]
1445+
fn parse_quoted_identifier_2() {
1446+
pg_and_generic().verified_stmt(r#"SELECT """quoted ident""""#);
1447+
}
1448+
14441449
#[test]
14451450
fn parse_local_and_global() {
14461451
pg_and_generic().verified_stmt("CREATE LOCAL TEMPORARY TABLE table (COL INT)");

0 commit comments

Comments
 (0)