From 3de2a9723c8701d3b5c404fc108ad4c46ef7bc7c Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:20:04 +0000 Subject: [PATCH 1/2] fix: preserve the `:format` suffix on interpolations in `fmt` --- prqlc/prqlc/src/codegen/ast.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/prqlc/prqlc/src/codegen/ast.rs b/prqlc/prqlc/src/codegen/ast.rs index 70350449f3ef..e7cbdf0319ae 100644 --- a/prqlc/prqlc/src/codegen/ast.rs +++ b/prqlc/prqlc/src/codegen/ast.rs @@ -501,9 +501,17 @@ fn display_interpolation( .replace('}', "}}") .as_str() } - pr::InterpolateItem::Expr { expr, .. } => { + // The `:format` suffix carries meaning — in `std.sql.prql` it's the + // required binding strength of the interpolated expression — so it + // has to be written back out. The parser reads it as "everything up + // to the closing brace", so it needs no escaping. + pr::InterpolateItem::Expr { expr, format } => { r += "{"; r += &expr.write(opt.clone())?; + if let Some(format) = format { + r += ":"; + r += format; + } r += "}" } } @@ -822,6 +830,24 @@ derive x = (foo `my arg`:5) assert_is_formatted( r#" prql version:"^0.9" target:sql.sqlite +"#, + ); + } + + /// The `:format` suffix on an interpolation used to be dropped on the floor. + /// In `std.sql.prql` that suffix is the operand's required binding strength, + /// so formatting the file rewrote `s"MIN({column:0})"` to `s"MIN({column})"` + /// and silently changed how the operator parenthesizes its argument. + #[test] + fn test_interpolation_format_is_preserved() { + assert_is_formatted(r#"let my_min = func column -> s"MIN({column:0})""#); + assert_is_formatted( + r#"let log = func base column -> s"LOG10({column:0}) / LOG10({base:0})""#, + ); + assert_is_formatted( + r#" +from t +derive x = f"{a:>10}-{b}" "#, ); } From a5463ef720d3dc2be1ecd01d8b7302218af80576 Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:30:27 +0000 Subject: [PATCH 2/2] fix: escape quotes and backslashes when writing a `:format` suffix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lexer strips string escapes before the interpolation parser runs, so writing the format back out verbatim re-emitted a raw `"` or `\` — output that either no longer parses or loses the escape on the next pass. Braces stay unescaped: the parser reads the format up to the closing brace without unescaping, so `f"{a:{}"` round-trips as-is. --- prqlc/prqlc/src/codegen/ast.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/prqlc/prqlc/src/codegen/ast.rs b/prqlc/prqlc/src/codegen/ast.rs index e7cbdf0319ae..88d919316647 100644 --- a/prqlc/prqlc/src/codegen/ast.rs +++ b/prqlc/prqlc/src/codegen/ast.rs @@ -504,13 +504,14 @@ fn display_interpolation( // The `:format` suffix carries meaning — in `std.sql.prql` it's the // required binding strength of the interpolated expression — so it // has to be written back out. The parser reads it as "everything up - // to the closing brace", so it needs no escaping. + // to the closing brace", so braces don't need escaping, but the + // string escapes the lexer removed do. pr::InterpolateItem::Expr { expr, format } => { r += "{"; r += &expr.write(opt.clone())?; if let Some(format) = format { r += ":"; - r += format; + r += &format.replace('\\', "\\\\").replace('"', "\\\""); } r += "}" } @@ -850,5 +851,13 @@ from t derive x = f"{a:>10}-{b}" "#, ); + // The lexer strips string escapes before the interpolation is parsed, so + // a quote or a backslash in the format has to be escaped on the way out — + // otherwise the quote terminates the string and the output doesn't parse. + assert_is_formatted(r#"let x = f"{a:\"q\"}""#); + assert_is_formatted(r#"let y = f"{a:\\}""#); + // Braces are the exception: the parser reads the format up to the closing + // brace without unescaping, so `{` round-trips as itself. + assert_is_formatted(r#"let z = f"{a:{}""#); } }