@@ -2069,7 +2069,7 @@ impl<'a> Tokenizer<'a> {
20692069 let error_loc = chars. location ( ) ;
20702070 chars. next ( ) ; // consume the opening quote
20712071 let quote_end = Word :: matching_end_quote ( quote_start) ;
2072- let ( s, last_char) = self . parse_quoted_ident ( chars, quote_end) ;
2072+ let ( s, last_char) = self . parse_quoted_ident ( chars, quote_end) ? ;
20732073
20742074 if last_char == Some ( quote_end) {
20752075 Ok ( s)
@@ -2368,11 +2368,17 @@ impl<'a> Tokenizer<'a> {
23682368 }
23692369 }
23702370
2371- fn parse_quoted_ident ( & self , chars : & mut State , quote_end : char ) -> ( String , Option < char > ) {
2371+ fn parse_quoted_ident (
2372+ & self ,
2373+ chars : & mut State ,
2374+ quote_end : char ,
2375+ ) -> Result < ( String , Option < char > ) , TokenizerError > {
2376+ let backslash_escape = self . dialect . supports_identifier_backslash_escape ( ) ;
23722377 let mut last_char = None ;
23732378 let mut s = String :: new ( ) ;
2374- while let Some ( ch) = chars. next ( ) {
2379+ while let Some ( & ch) = chars. peek ( ) {
23752380 if ch == quote_end {
2381+ chars. next ( ) ;
23762382 if chars. peek ( ) == Some ( & quote_end) {
23772383 chars. next ( ) ;
23782384 s. push ( ch) ;
@@ -2384,11 +2390,35 @@ impl<'a> Tokenizer<'a> {
23842390 last_char = Some ( quote_end) ;
23852391 break ;
23862392 }
2393+ } else if ch == '\\' && backslash_escape {
2394+ let escape_loc = chars. location ( ) ;
2395+ chars. next ( ) ; // consume the backslash
2396+ let Some ( escaped) = chars. next ( ) else {
2397+ // EOF right after the backslash: leave the loop so the
2398+ // caller reports the missing close delimiter.
2399+ break ;
2400+ } ;
2401+ if self . unescape {
2402+ match unescape_identifier_escape ( escaped) {
2403+ Some ( resolved) => s. push ( resolved) ,
2404+ None => {
2405+ return self . tokenizer_error (
2406+ escape_loc,
2407+ format ! ( "Syntax error: Illegal escape sequence: \\ {escaped}" ) ,
2408+ ) ;
2409+ }
2410+ }
2411+ } else {
2412+ // In no-escape mode, keep the query text verbatim.
2413+ s. push ( '\\' ) ;
2414+ s. push ( escaped) ;
2415+ }
23872416 } else {
2417+ chars. next ( ) ;
23882418 s. push ( ch) ;
23892419 }
23902420 }
2391- ( s, last_char)
2421+ Ok ( ( s, last_char) )
23922422 }
23932423
23942424 #[ allow( clippy:: unnecessary_wraps) ]
@@ -2405,6 +2435,24 @@ impl<'a> Tokenizer<'a> {
24052435/// Read from `chars` until `predicate` returns `false` or EOF is hit.
24062436/// Return the characters read as String, and keep the first non-matching
24072437/// char available as `chars.next()`.
2438+ /// Resolve a single-character GoogleSQL escape sequence inside a quoted
2439+ /// identifier. Returns `None` for an unrecognized escape, which the caller
2440+ /// reports as an illegal escape sequence.
2441+ /// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences>
2442+ fn unescape_identifier_escape ( ch : char ) -> Option < char > {
2443+ match ch {
2444+ 'a' => Some ( '\u{7}' ) ,
2445+ 'b' => Some ( '\u{8}' ) ,
2446+ 'f' => Some ( '\u{c}' ) ,
2447+ 'n' => Some ( '\n' ) ,
2448+ 'r' => Some ( '\r' ) ,
2449+ 't' => Some ( '\t' ) ,
2450+ 'v' => Some ( '\u{b}' ) ,
2451+ '\\' | '?' | '"' | '\'' | '`' => Some ( ch) ,
2452+ _ => None ,
2453+ }
2454+ }
2455+
24082456fn peeking_take_while ( chars : & mut State , mut predicate : impl FnMut ( char ) -> bool ) -> String {
24092457 let mut s = String :: new ( ) ;
24102458 while let Some ( & ch) = chars. peek ( ) {
@@ -3714,6 +3762,54 @@ mod tests {
37143762 compare ( expected, tokens) ;
37153763 }
37163764
3765+ #[ test]
3766+ fn tokenize_bigquery_quoted_identifier_backslash_escape ( ) {
3767+ // GoogleSQL resolves backslash escapes inside backtick identifiers:
3768+ // `\`` -> a literal backtick, raw double quotes pass through.
3769+ let sql = r#"`a "very" weird \`name\``"# ;
3770+ let dialect = BigQueryDialect { } ;
3771+ let tokens = Tokenizer :: new ( & dialect, sql) . tokenize ( ) . unwrap ( ) ;
3772+ let expected = vec ! [ Token :: make_word( r#"a "very" weird `name`"# , Some ( '`' ) ) ] ;
3773+ compare ( expected, tokens) ;
3774+ }
3775+
3776+ #[ test]
3777+ fn tokenize_bigquery_quoted_identifier_backslash_escape_no_unescape ( ) {
3778+ // With unescaping disabled the identifier text is preserved verbatim.
3779+ let sql = r#"`a \`b`"# ;
3780+ let dialect = BigQueryDialect { } ;
3781+ let tokens = Tokenizer :: new ( & dialect, sql)
3782+ . with_unescape ( false )
3783+ . tokenize ( )
3784+ . unwrap ( ) ;
3785+ let expected = vec ! [ Token :: make_word( r#"a \`b"# , Some ( '`' ) ) ] ;
3786+ compare ( expected, tokens) ;
3787+ }
3788+
3789+ #[ test]
3790+ fn tokenize_bigquery_quoted_identifier_illegal_escape ( ) {
3791+ let sql = r#"`bad\qescape`"# ;
3792+ let dialect = BigQueryDialect { } ;
3793+ let mut tokenizer = Tokenizer :: new ( & dialect, sql) ;
3794+ assert_eq ! (
3795+ tokenizer. tokenize( ) ,
3796+ Err ( TokenizerError {
3797+ message: r#"Syntax error: Illegal escape sequence: \q"# . to_string( ) ,
3798+ location: Location { line: 1 , column: 5 } ,
3799+ } )
3800+ ) ;
3801+ }
3802+
3803+ #[ test]
3804+ fn tokenize_generic_quoted_identifier_no_backslash_escape ( ) {
3805+ // Dialects without GoogleSQL escapes leave the backslash untouched.
3806+ let sql = r#"`a\b`"# ;
3807+ let dialect = GenericDialect { } ;
3808+ let tokens = Tokenizer :: new ( & dialect, sql) . tokenize ( ) . unwrap ( ) ;
3809+ let expected = vec ! [ Token :: make_word( r"a\b" , Some ( '`' ) ) ] ;
3810+ compare ( expected, tokens) ;
3811+ }
3812+
37173813 #[ test]
37183814 fn tokenize_with_location ( ) {
37193815 let sql = "SELECT a,\n b" ;
0 commit comments