From f55f77a63a4e827ec26f4af7b41693eef6e84e06 Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:18:21 +0000 Subject: [PATCH 1/2] fix: correct byte indexing for non-ASCII Jinja interpolations find_span used char indices from chars().enumerate() to slice source by bytes, corrupting (or panicking on) spans when the source contained multi-byte characters before an interpolation. Use char_indices() and len_utf8() so the slice stays on char boundaries. --- prqlc/prqlc/src/cli/jinja.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/prqlc/prqlc/src/cli/jinja.rs b/prqlc/prqlc/src/cli/jinja.rs index 3d8f219a8aaa..6b4722fff4ae 100644 --- a/prqlc/prqlc/src/cli/jinja.rs +++ b/prqlc/prqlc/src/cli/jinja.rs @@ -98,7 +98,9 @@ fn find_span(source: &str, spans: Vec) -> &str { let mut line = 1; let mut col = 0; - for (index, char) in source.chars().enumerate() { + // `char_indices` yields byte offsets, so the slice below stays on char + // boundaries even when `source` contains multi-byte characters. + for (index, char) in source.char_indices() { if char == '\n' { line += 1; col = 0; @@ -111,7 +113,7 @@ fn find_span(source: &str, spans: Vec) -> &str { start_index = index; } if line == end.end_line && col == end.end_col { - end_index = index + 1; + end_index = index + char.len_utf8(); } } &source[start_index..end_index] @@ -198,6 +200,17 @@ mod test { insta::assert_yaml_snapshot!(post_proc_text, @r#""from in_process = {{ source('salesforce', 'in_process') }}""#); } + #[test] + fn test_non_ascii_before_interpolation() { + // Non-ASCII bytes before an interpolation must not cause a panic or a + // mis-sliced span. `é` is two bytes but one char, so a char-indexed + // slice would land mid-character. + let src = r###"from café = {{ source('salesforce', 'in_process') }}"###; + let (pre_proc_text, ctx) = pre_process(src).unwrap(); + insta::assert_yaml_snapshot!(pre_proc_text, @"from café = _jinja_0"); + insta::assert_yaml_snapshot!(ctx.anchor_map["_jinja_0"], @r#"" {{ source('salesforce', 'in_process') }}""#); + } + #[test] fn test_config_interpolation() { let src = r###"{{ config(materialized = "table") }}\nfrom in_process = {{ source('salesforce', 'in_process') }}"###; From 7f7f20f2c0c3f6d81b424fa74d3fd9de40e27d99 Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:14:24 +0000 Subject: [PATCH 2/2] fix: reword comment to satisfy typos --- prqlc/prqlc/src/cli/jinja.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prqlc/prqlc/src/cli/jinja.rs b/prqlc/prqlc/src/cli/jinja.rs index 6b4722fff4ae..4fe23211dd37 100644 --- a/prqlc/prqlc/src/cli/jinja.rs +++ b/prqlc/prqlc/src/cli/jinja.rs @@ -203,7 +203,7 @@ mod test { #[test] fn test_non_ascii_before_interpolation() { // Non-ASCII bytes before an interpolation must not cause a panic or a - // mis-sliced span. `é` is two bytes but one char, so a char-indexed + // corrupted span. `é` is two bytes but one char, so a char-indexed // slice would land mid-character. let src = r###"from café = {{ source('salesforce', 'in_process') }}"###; let (pre_proc_text, ctx) = pre_process(src).unwrap();