diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 20058b83a..d87fa6b16 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -382,8 +382,16 @@ impl fmt::Display for Ident { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.quote_style { Some(q) if q == '"' || q == '\'' || q == '`' => { - let escaped = value::escape_quoted_string(&self.value, q); - write!(f, "{q}{escaped}{q}") + // The value is the decoded identifier, so every delimiter + // inside it is literal and must be doubled. + write!(f, "{q}")?; + for (i, part) in self.value.split(q).enumerate() { + if i > 0 { + write!(f, "{q}{q}")?; + } + f.write_str(part)?; + } + write!(f, "{q}") } Some('[') => write!(f, "[{}]", self.value), None => f.write_str(&self.value), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2de6062b2..49ce206b7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20060,3 +20060,46 @@ fn parse_insert_by_name() { _ => unreachable!(), } } + +#[test] +fn parse_quoted_identifier_with_consecutive_delimiters() { + let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('"')); + let select = dialects.verified_only_select(r#"SELECT "a""""b""#); + assert_eq!( + &Expr::Identifier(Ident::with_quote('"', r#"a""b"#)), + expr_from_projection(&select.projection[0]), + ); + + let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('`')); + let select = dialects.verified_only_select("SELECT `a````b`"); + assert_eq!( + &Expr::Identifier(Ident::with_quote('`', "a``b")), + expr_from_projection(&select.projection[0]), + ); +} + +#[test] +fn quoted_identifier_display_round_trips() { + // Every value of length 1..=4 over the delimiter, a backslash, an ASCII + // and a multi-byte character, serialized and parsed back. + for quote in ['"', '`'] { + let dialects = all_dialects_where(move |d| d.is_delimited_identifier_start(quote)); + let mut values = vec![String::new()]; + for _ in 0..4 { + values = values + .iter() + .flat_map(|v| [quote, '\\', 'a', 'é'].map(|c| format!("{v}{c}"))) + .collect(); + for value in &values { + let ident = Ident::with_quote(quote, value.as_str()); + let sql = format!("SELECT {ident}"); + let select = dialects.verified_only_select(&sql); + assert_eq!( + &Expr::Identifier(ident), + expr_from_projection(&select.projection[0]), + "{sql} did not round trip", + ); + } + } + } +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 97f213743..916111f15 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1514,7 +1514,9 @@ fn parse_escaped_quote_identifiers_with_no_escape() { require_semicolon_stmt_delimiter: true, } ) - .verified_stmt(sql), + .parse_sql_statements(sql) + .unwrap() + .remove(0), Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -1616,7 +1618,9 @@ fn parse_escaped_backticks_with_no_escape() { vec![Box::new(MySqlDialect {})], ParserOptions::new().with_unescape(false) ) - .verified_stmt(sql), + .parse_sql_statements(sql) + .unwrap() + .remove(0), Statement::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select {