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