diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8a9a67a74..e5f98b097 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -385,7 +385,28 @@ impl fmt::Display for Ident { let escaped = value::escape_quoted_string(&self.value, q); write!(f, "{q}{escaped}{q}") } - Some('[') => write!(f, "[{}]", self.value), + Some('[') => { + let v = &self.value; + if v.len() >= 2 && v.starts_with('"') && v.ends_with('"') { + // A nested double-quoted identifier (e.g. Redshift `["a]b"]`) + // keeps its inner quotes in the value and its `]` is already + // literal, so emit it unchanged. + write!(f, "[{v}]") + } else { + // Double a lone `]` so the identifier round-trips, but leave + // an already-doubled `]]` intact: in the tokenizer's no-escape + // mode the value still holds the raw `]]`, and re-doubling it + // would corrupt the identifier. + write!(f, "[")?; + let mut rest = v.as_str(); + while let Some(pos) = rest.find(']') { + write!(f, "{}]]", &rest[..pos])?; + let after = &rest[pos + 1..]; + rest = after.strip_prefix(']').unwrap_or(after); + } + write!(f, "{rest}]") + } + } None => f.write_str(&self.value), _ => panic!("unexpected quote style"), } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 3faf56f0d..3959fc3ff 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -937,6 +937,32 @@ fn parse_table_name_in_square_brackets() { ); } +#[test] +fn parse_bracket_identifier_with_escaped_closing_bracket() { + // A `]` inside a bracket-quoted identifier is escaped by doubling it, so + // `[a]]b]` denotes the identifier `a]b`. + let select = ms().verified_only_select("SELECT [a]]b]"); + assert_eq!( + &Expr::Identifier(Ident::with_quote('[', "a]b")), + expr_from_projection(&select.projection[0]), + ); + + // Round-trips regardless of where the escaped `]` sits. + for sql in [ + "SELECT [a]]b] FROM [c]]d]", + "SELECT []]]", // the identifier is a single `]` + "SELECT [a]]]", // trailing `]` + "SELECT []]b]", // leading `]` + "SELECT [a]]b]]c]", // several escaped `]` + ] { + ms().verified_stmt(sql); + } + + // In no-escape mode the parsed value keeps the raw `]]`, so serializing it + // must not double the brackets again. + ms_no_unescape().verified_stmt("SELECT [a]]b]"); +} + #[test] fn parse_for_clause() { ms_and_generic().verified_stmt("SELECT a FROM t FOR JSON PATH"); @@ -2435,6 +2461,13 @@ fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } +fn ms_no_unescape() -> TestedDialects { + TestedDialects::new_with_options( + vec![Box::new(MsSqlDialect {})], + ParserOptions::new().with_unescape(false), + ) +} + // MS SQL dialect with support for optional semi-colon statement delimiters fn tsql() -> TestedDialects { TestedDialects::new_with_options(