This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 705e6405b5 fix(variant): support closing brackets inside quoted path 
fields (#11231)
705e6405b5 is described below

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;
             }

Reply via email to