This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11231-d7b9bf431c980ccad21b0a9777e12d24770cad58 in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit 705e6405b56cc01efb1c267203d240d0d85804ab Author: Bryan Dang <[email protected]> AuthorDate: Fri Oct 2 21:40:27 2026 +0000 fix(variant): support closing brackets inside quoted path fields (#11231) # Which issue does this PR close? - Closes #11228. # Rationale for this change Quoted VariantPath fields can contain a literal closing bracket, but the parser currently terminates the bracket segment at the first unescaped `]`, even when it appears inside matching quotes. This rejects paths such as `["a]b"]` and can silently split some field names into multiple path elements. # What changes are included in this PR? - Track the active quote while parsing a bracket path element. - Treat an unescaped `]` as the closing delimiter only outside matching quotes. - Preserve the existing `\]` escape behavior. - Document the quote-aware behavior. - Add regression coverage for single and double quotes, subsequent path elements, escaped quotes, and the legacy escaped spelling. # Are these changes tested? Yes. The following checks pass: - `cargo test -p parquet-variant` - `cargo clippy -p parquet-variant --all-targets --all-features -- -D warnings` - `cargo +stable fmt --all -- --check` - `git diff --check` # Are there any user-facing changes? Yes. Quoted VariantPath fields may now contain an unescaped `]`. Existing paths using `\]` remain supported. There are no public API changes. # AI usage OpenAI Codex was used to inspect the existing parser, help implement the quote-state change, add regression tests, and run validation. I reviewed the resulting code and test behavior and can own and debug the change. Co-authored-by: Kosta Tarasov <[email protected]> --- parquet-variant/src/path.rs | 25 +++++++++++++++++++++++++ parquet-variant/src/utils.rs | 15 +++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/parquet-variant/src/path.rs b/parquet-variant/src/path.rs index 64aca085c5..98a6e916d8 100644 --- a/parquet-variant/src/path.rs +++ b/parquet-variant/src/path.rs @@ -322,6 +322,31 @@ mod tests { VariantPathElement::field("1"), ]); assert_eq!(path, expected); + + // an unescaped closing bracket is literal inside a quoted field + for input in [r#"["a]b"]"#, "['a]b']"] { + let path = VariantPath::try_from(input).unwrap(); + let expected = VariantPath::from_iter([VariantPathElement::field("a]b")]); + assert_eq!(path, expected); + } + + // quoted brackets do not consume the following path element + let path = VariantPath::try_from(r#"["[0]"][1]"#).unwrap(); + let expected = VariantPath::from_iter([ + VariantPathElement::field("[0]"), + VariantPathElement::index(1), + ]); + assert_eq!(path, expected); + + // escaped quotes and an unescaped bracket remain one quoted field + let path = VariantPath::try_from(r#"["\"][\""]"#).unwrap(); + let expected = VariantPath::from_iter([VariantPathElement::field(r#""][""#)]); + assert_eq!(path, expected); + + // the existing escaped spelling remains supported + let path = VariantPath::try_from(r#"["a\]b"]"#).unwrap(); + let expected = VariantPath::from_iter([VariantPathElement::field("a]b")]); + assert_eq!(path, expected); } #[test] diff --git a/parquet-variant/src/utils.rs b/parquet-variant/src/utils.rs index 782f718cfc..049ab82c99 100644 --- a/parquet-variant/src/utils.rs +++ b/parquet-variant/src/utils.rs @@ -157,6 +157,7 @@ pub(crate) const fn expect_size_of<T>(expected: usize) { /// - `\\` -> literal `\` /// - `\]` -> literal `]` /// - Any other `\x` -> literal `x` +/// - An unescaped `]` inside a quoted field is literal /// /// Outside brackets, no escaping is supported. /// @@ -169,6 +170,7 @@ pub(crate) const fn expect_size_of<T>(expected: usize) { /// - `"foo[1].bar"` -> field `foo`, index 1, field `bar` /// - `"['a.b']"` -> field `a.b` (dot is literal inside bracket) /// - `"['a\]b']"` -> field `a]b` (escaped `]` +/// - `"['a]b']"` -> field `a]b` (`]` is literal inside quotes) /// - etc. /// /// # Errors @@ -235,6 +237,7 @@ fn parse_in_bracket(s: &str, i: usize) -> Result<(VariantPathElement<'_>, usize) let mut unescaped = String::new(); let mut chars = s[start..].char_indices(); let mut end = None; + let mut quote = None; while let Some((offset, c)) = chars.next() { match c { @@ -245,8 +248,16 @@ fn parse_in_bracket(s: &str, i: usize) -> Result<(VariantPathElement<'_>, usize) } // Trailing backslash will be handled as 'unclosed [' below } - ']' => { - // Unescaped ']' ends the bracket + '\'' | '"' if quote == Some(c) => { + quote = None; + unescaped.push(c); + } + '\'' | '"' if quote.is_none() && unescaped.is_empty() => { + quote = Some(c); + unescaped.push(c); + } + ']' if quote.is_none() => { + // An unescaped ']' outside quotes ends the bracket end = Some(start + offset); break; }
