diff --git a/prqlc/prqlc-parser/src/lexer/mod.rs b/prqlc/prqlc-parser/src/lexer/mod.rs index b59753d8ceaa..9acc5c2700e1 100644 --- a/prqlc/prqlc-parser/src/lexer/mod.rs +++ b/prqlc/prqlc-parser/src/lexer/mod.rs @@ -222,7 +222,7 @@ fn param<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { fn interpolation<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { // For s-strings and f-strings, use the same multi-quote string parser - // No escaping for interpolated strings + // Enable escaping so that `\"` in the source becomes a literal `"` in the string // // NOTE: Known limitation in error reporting for unclosed interpolated strings: // When an f-string or s-string is unclosed (e.g., `f"{}`), the error is reported at the @@ -231,7 +231,7 @@ fn interpolation<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserErro // modifies error spans during error recovery, and there's no way to prevent this from // custom parsers. one_of("sf") - .then(quoted_string(false)) + .then(quoted_string(true)) .map(|(c, s)| TokenKind::Interpolation(c, s)) } diff --git a/prqlc/prqlc-parser/src/lexer/test.rs b/prqlc/prqlc-parser/src/lexer/test.rs index 6eb69616536f..59d909257dee 100644 --- a/prqlc/prqlc-parser/src/lexer/test.rs +++ b/prqlc/prqlc-parser/src/lexer/test.rs @@ -178,6 +178,15 @@ fn interpolated_strings() { ], ) "#); + + // Test s-string with escaped quotes (issue #5494 regression) + assert_debug_snapshot!(test_interpolation_tokens(r#"s"SELECT \"col1 foo\"""#), @r#" + Tokens( + [ + 0..22: Interpolation('s', "SELECT \"col1 foo\""), + ], + ) + "#); } #[test] diff --git a/prqlc/prqlc/src/codegen/ast.rs b/prqlc/prqlc/src/codegen/ast.rs index fee2eed058c9..64aea84c8f6e 100644 --- a/prqlc/prqlc/src/codegen/ast.rs +++ b/prqlc/prqlc/src/codegen/ast.rs @@ -479,8 +479,15 @@ fn display_interpolation( r += "\""; for part in parts { match &part { - // We use double braces to escape braces - pr::InterpolateItem::String(s) => r += s.replace('{', "{{").replace('}', "}}").as_str(), + // We use double braces to escape braces and backslash-quote to escape quotes + pr::InterpolateItem::String(s) => { + r += s + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('{', "{{") + .replace('}', "}}") + .as_str() + } pr::InterpolateItem::Expr { expr, .. } => { r += "{"; r += &expr.write(opt.clone())?; @@ -578,6 +585,12 @@ mod test { ); } + #[test] + fn test_sstring_escaped_quotes() { + // Test that escaped quotes in s-strings round-trip correctly (issue #5496) + assert_is_formatted(r#"from s"SELECT \"col1 foo\"""#); + } + #[test] fn test_unary() { assert_is_formatted(r#"sort {-duration}"#);