sunchao commented on code in PR #4971:
URL: https://github.com/apache/datafusion-comet/pull/4971#discussion_r4100699856


##########
native/spark-expr/src/string_funcs/get_json_object.rs:
##########
@@ -372,25 +644,107 @@ impl<'de> Visitor<'de> for SegmentVisitor<'_> {
     }
 
     fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, 
A::Error> {
-        let PathSegment::Index(idx) = &self.segments[0] else {
-            IgnoredAny.visit_seq(seq)?;
-            return Ok(None);
-        };
-
-        for _ in 0..*idx {
-            if seq.next_element::<IgnoredAny>()?.is_none() {
-                return Ok(None);
+        match &self.segments[0] {
+            PathSegment::Index(idx) => {
+                // Spark switches to Quoted style for the "one or more results"
+                // case: an index immediately followed by a subscript wildcard.
+                let child_style = match self.segments.get(1) {
+                    Some(PathSegment::SubscriptWildcard) | 
Some(PathSegment::DoubleWildcard) => {
+                        Style::Quoted
+                    }
+                    _ => self.style,
+                };
+                for _ in 0..*idx {
+                    if seq.next_element::<IgnoredAny>()?.is_none() {
+                        return Ok(PathResult::default());
+                    }
+                }
+                let found = seq
+                    .next_element_seed(PathSeed {
+                        segments: &self.segments[1..],
+                        style: child_style,
+                        reject_direct_null: false,
+                    })?
+                    .unwrap_or_default();
+                // The remaining elements are still visited, so that a 
malformed element
+                // after the match yields no match, as a full parse would.
+                IgnoredAny.visit_seq(seq)?;
+                Ok(found)
+            }
+            PathSegment::DoubleWildcard => {
+                // Spark consumes both wildcards of `[*][*]` at once: the
+                // remaining path applies to the outer elements in flatten
+                // style, and the collected writes always form a single array,
+                // even when there is only one element or none matched.
+                let mut writes = SmallVec::new();
+                let mut matched = false;
+                while let Some(mut result) = seq.next_element_seed(PathSeed {
+                    segments: &self.segments[1..],
+                    style: Style::Flatten,
+                    reject_direct_null: false,
+                })? {
+                    matched |= result.matched;
+                    writes.append(&mut result.writes);
+                }
+                Ok(PathResult::wrap(writes, matched))
+            }
+            PathSegment::SubscriptWildcard => match self.style {
+                // Quoted style: the array wrapper is always kept, even for a
+                // single match.
+                Style::Quoted => {
+                    let mut writes = SmallVec::new();
+                    let mut matched = false;
+                    while let Some(mut result) = 
seq.next_element_seed(PathSeed {
+                        segments: &self.segments[1..],
+                        style: Style::Quoted,
+                        reject_direct_null: false,
+                    })? {
+                        matched |= result.matched;
+                        writes.append(&mut result.writes);
+                    }
+                    Ok(PathResult::wrap(writes, matched))
+                }
+                // Raw or Flatten style: Spark buffers the element writes into
+                // a temporary array and only emits it when more than one
+                // element wrote; a lone writer's brackets are stripped, and
+                // nothing at all is written when no element matched.
+                Style::Raw | Style::Flatten => {
+                    let child_style = if self.style == Style::Raw {
+                        Style::Quoted
+                    } else {
+                        Style::Flatten
+                    };
+                    let mut writers = 0;
+                    let mut writes = SmallVec::new();
+                    while let Some(mut result) = 
seq.next_element_seed(PathSeed {

Review Comment:
   [P2] Preserve Spark's nesting limit when streaming wildcard paths
   
   For `$[*].a`, construct the valid JSON as `'[{"a":1,"skip":' + '[' * 999 + 
'0' + ']' * 999 + '}]'`. Its total nesting depth is 1,001. Spark 3.5.9, 4.0.4, 
4.1.3, and the PR base return SQL NULL, while this head returns `1`. I 
reproduced the head result through the compiled scalar and both column entry 
points. Reducing the inner arrays to 998 makes Spark and head return `1`; 
either field order reproduces the boundary.
   
   The new wildcard traversal skips unrelated subtrees through `IgnoredAny`, 
which does not enforce Jackson's depth constraint. This extends a pre-existing 
nonwildcard limitation to wildcard paths that the base rejected.
   
   Could we enforce the applicable Spark version's nesting limit while skipping 
subtrees and add both sides of this boundary? Spark 3.4.3 accepts these 
documents, so the check needs to be version-aware. Restoring serde's older 
128-level limit would be too restrictive.



##########
native/spark-expr/src/string_funcs/get_json_object.rs:
##########
@@ -246,68 +306,269 @@ fn parse_json_path(path: &str) -> Option<ParsedPath> {
         }
     }
 
-    Some(ParsedPath {
-        segments,
-        has_wildcard,
-    })
+    Some(ParsedPath { segments })
+}
+
+/// Find the end of a string body starting at `i` (just past the opening
+/// quote). Short bodies are scanned inline; long bodies use memchr2, the same
+/// approach as serde_json's `ignore_str`. Returns the index just past the
+/// closing quote, or None for an unterminated string (the parser rejects the
+/// document anyway).
+#[inline]
+fn skip_string_body(bytes: &[u8], mut i: usize) -> Option<usize> {
+    const SHORT_STRING: usize = 32;
+    if bytes.len().checked_sub(i)? <= SHORT_STRING {
+        while i < bytes.len() {
+            match bytes[i] {
+                b'"' => return Some(i + 1),
+                b'\\' => i = i.checked_add(2)?,
+                _ => i += 1,
+            }
+        }
+        return None;
+    }
+    loop {
+        match memchr::memchr2(b'"', b'\\', bytes.get(i..)?) {
+            Some(off) if bytes[i + off] == b'"' => return Some(i + off + 1),
+            Some(off) => i = i.checked_add(off)?.checked_add(2)?, // escaped 
byte
+            None => return None,
+        }
+    }
+}
+
+/// Spark 3.5+ bundles Jackson versions that reject numbers beyond the default
+/// 1000-digit limit, including values this evaluation skips. serde_json's
+/// `IgnoredAny` enforces no such limit, so inspect number tokens before 
parsing.
+/// Spark 3.4's Jackson has no default limit and bypasses this scan.
+fn has_oversized_number(json: &str) -> bool {
+    const MAX_NUMBER_DIGITS: usize = 1000;
+    const JACKSON_READER_BUFFER_UNITS: usize = 4000;
+    let bytes = json.as_bytes();
+    let mut i = 0;
+    // Only near-limit floats need the Reader's UTF-16 position. Keep the
+    // prefix count between candidates so many long numbers stay linear.
+    let mut counted_through = 0;
+    let mut utf16_units = 0;
+    while i < bytes.len() {
+        match bytes[i] {
+            // Skip string bodies: Jackson applies no numeric constraint to
+            // string content.
+            b'"' => match skip_string_body(bytes, i + 1) {
+                Some(end) => i = end,
+                None => return false,
+            },
+            b'-' | b'0'..=b'9' => {
+                let mut j = i + usize::from(bytes[i] == b'-');
+                let int_start = j;
+                while j < bytes.len() && bytes[j].is_ascii_digit() {
+                    j += 1;
+                }
+                let int_len = j - int_start;
+                let mut fract_len = 0;
+                if j < bytes.len() && bytes[j] == b'.' {
+                    j += 1;
+                    let start = j;
+                    while j < bytes.len() && bytes[j].is_ascii_digit() {
+                        j += 1;
+                    }
+                    fract_len = j - start;
+                }
+                let mut exp_len = 0;
+                if j < bytes.len() && (bytes[j] | 0x20) == b'e' {
+                    j += 1;
+                    if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == 
b'-') {
+                        j += 1;
+                    }
+                    let start = j;
+                    while j < bytes.len() && bytes[j].is_ascii_digit() {
+                        j += 1;
+                    }
+                    exp_len = j - start;
+                }
+                let is_float = fract_len > 0 || exp_len > 0;
+                let digit_count = if is_float {
+                    // Spark parses UTF8String via InputStreamReader, then
+                    // ReaderBasedJsonParser with a 4000-UTF-16-unit buffer.
+                    // Its slow path uses -1 for an absent fraction or 
exponent,
+                    // forgiving one digit when a number reaches a buffer edge
+                    // or EOF. Numbers starting with zero always take that 
path.
+                    let starts_with_zero = int_len == 1 && bytes[int_start] == 
b'0';
+                    let reaches_buffer_edge = if int_len + fract_len + exp_len 
> MAX_NUMBER_DIGITS {
+                        utf16_units += 
json[counted_through..i].encode_utf16().count();
+                        counted_through = i;
+                        utf16_units % JACKSON_READER_BUFFER_UNITS + (j - i)

Review Comment:
   [P2] Account for recycled Jackson buffers in numeric validation
   
   This assumes every reader buffer has 4,000 UTF-16 units, but Jackson can 
reuse a larger buffer allocated by an earlier String parser. For 
`{"a":1,"pad":"<5000 x characters>","n":1.<1000 zeros>}`, `$.a` and `$.n` 
return `1` and `1.0` on the base and actual Spark with a recycled 
6,000-character buffer. This head returns NULL for both, including through the 
compiled scalar and both column entry points.
   
   I reproduced the larger-buffer behavior on Spark 3.5.9, 4.0.4, and 4.1.3 
evaluators. It also occurs in ordinary Spark 4.1.3 SQL: read a 6,000-character 
JSON Dataset row, build this JSON from a nonconstant input column, then apply 
`get_json_object`. It reproduces with whole-stage codegen disabled and enabled. 
[JsonFactory allocates the String parser buffer by input 
length](https://github.com/FasterXML/jackson-core/blob/jackson-core-2.21.2/src/main/java/com/fasterxml/jackson/core/JsonFactory.java#L1362-L1373),
 and [BufferRecycler retains larger 
buffers](https://github.com/FasterXML/jackson-core/blob/jackson-core-2.21.2/src/main/java/com/fasterxml/jackson/core/util/BufferRecycler.java#L190-L207).
   
   Could we revisit this fixed-buffer compatibility model and cover the JSON 
Dataset pipeline? A fixed modulo cannot recover the JVM recycler state from the 
JSON text. An explicit compatibility policy for these rare long-number inputs 
would also be clearer than claiming reader-boundary parity.



##########
native/spark-expr/src/string_funcs/get_json_object.rs:
##########
@@ -246,68 +306,269 @@ fn parse_json_path(path: &str) -> Option<ParsedPath> {
         }
     }
 
-    Some(ParsedPath {
-        segments,
-        has_wildcard,
-    })
+    Some(ParsedPath { segments })
+}
+
+/// Find the end of a string body starting at `i` (just past the opening
+/// quote). Short bodies are scanned inline; long bodies use memchr2, the same
+/// approach as serde_json's `ignore_str`. Returns the index just past the
+/// closing quote, or None for an unterminated string (the parser rejects the
+/// document anyway).
+#[inline]
+fn skip_string_body(bytes: &[u8], mut i: usize) -> Option<usize> {
+    const SHORT_STRING: usize = 32;
+    if bytes.len().checked_sub(i)? <= SHORT_STRING {
+        while i < bytes.len() {
+            match bytes[i] {
+                b'"' => return Some(i + 1),
+                b'\\' => i = i.checked_add(2)?,
+                _ => i += 1,
+            }
+        }
+        return None;
+    }
+    loop {
+        match memchr::memchr2(b'"', b'\\', bytes.get(i..)?) {
+            Some(off) if bytes[i + off] == b'"' => return Some(i + off + 1),
+            Some(off) => i = i.checked_add(off)?.checked_add(2)?, // escaped 
byte
+            None => return None,
+        }
+    }
+}
+
+/// Spark 3.5+ bundles Jackson versions that reject numbers beyond the default
+/// 1000-digit limit, including values this evaluation skips. serde_json's
+/// `IgnoredAny` enforces no such limit, so inspect number tokens before 
parsing.
+/// Spark 3.4's Jackson has no default limit and bypasses this scan.
+fn has_oversized_number(json: &str) -> bool {
+    const MAX_NUMBER_DIGITS: usize = 1000;
+    const JACKSON_READER_BUFFER_UNITS: usize = 4000;
+    let bytes = json.as_bytes();
+    let mut i = 0;
+    // Only near-limit floats need the Reader's UTF-16 position. Keep the
+    // prefix count between candidates so many long numbers stay linear.
+    let mut counted_through = 0;
+    let mut utf16_units = 0;
+    while i < bytes.len() {
+        match bytes[i] {
+            // Skip string bodies: Jackson applies no numeric constraint to
+            // string content.
+            b'"' => match skip_string_body(bytes, i + 1) {
+                Some(end) => i = end,
+                None => return false,
+            },
+            b'-' | b'0'..=b'9' => {
+                let mut j = i + usize::from(bytes[i] == b'-');
+                let int_start = j;
+                while j < bytes.len() && bytes[j].is_ascii_digit() {
+                    j += 1;
+                }
+                let int_len = j - int_start;
+                let mut fract_len = 0;
+                if j < bytes.len() && bytes[j] == b'.' {
+                    j += 1;
+                    let start = j;
+                    while j < bytes.len() && bytes[j].is_ascii_digit() {
+                        j += 1;
+                    }
+                    fract_len = j - start;
+                }
+                let mut exp_len = 0;
+                if j < bytes.len() && (bytes[j] | 0x20) == b'e' {
+                    j += 1;
+                    if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == 
b'-') {
+                        j += 1;
+                    }
+                    let start = j;
+                    while j < bytes.len() && bytes[j].is_ascii_digit() {
+                        j += 1;
+                    }
+                    exp_len = j - start;
+                }
+                let is_float = fract_len > 0 || exp_len > 0;
+                let digit_count = if is_float {
+                    // Spark parses UTF8String via InputStreamReader, then
+                    // ReaderBasedJsonParser with a 4000-UTF-16-unit buffer.
+                    // Its slow path uses -1 for an absent fraction or 
exponent,
+                    // forgiving one digit when a number reaches a buffer edge
+                    // or EOF. Numbers starting with zero always take that 
path.
+                    let starts_with_zero = int_len == 1 && bytes[int_start] == 
b'0';
+                    let reaches_buffer_edge = if int_len + fract_len + exp_len 
> MAX_NUMBER_DIGITS {
+                        utf16_units += 
json[counted_through..i].encode_utf16().count();
+                        counted_through = i;
+                        utf16_units % JACKSON_READER_BUFFER_UNITS + (j - i)
+                            >= JACKSON_READER_BUFFER_UNITS
+                    } else {
+                        false
+                    };
+                    let slow_path = starts_with_zero || reaches_buffer_edge || 
j == bytes.len();
+                    let absent_component = fract_len == 0 || exp_len == 0;
+                    int_len + fract_len + exp_len - usize::from(slow_path && 
absent_component)
+                } else {
+                    int_len
+                };
+                if digit_count > MAX_NUMBER_DIGITS {
+                    return true;
+                }
+                i = j;
+            }
+            _ => i += 1,
+        }
+    }
+    false
 }
 
 /// Evaluate a parsed JSONPath against a JSON string.
 /// Returns the result as a string, or None if no match.
+#[cfg(test)]
 fn evaluate_path(json_str: &str, path: &ParsedPath) -> Option<String> {
-    if !path.has_wildcard {
-        return value_into_string(extract_no_wildcard(json_str, 
&path.segments)?);
-    }
-
-    let value: Value = serde_json::from_str(json_str).ok()?;
+    evaluate_path_with_number_limit(json_str, path, true)
+}
 
-    // Wildcard path: may return multiple results
-    let results = evaluate_with_wildcard(&value, &path.segments);
+fn evaluate_path_with_number_limit(
+    json_str: &str,
+    path: &ParsedPath,
+    check_number_length: bool,
+) -> Option<String> {
+    if check_number_length && has_oversized_number(json_str) {

Review Comment:
   [P2] Skip numeric validation for inputs too short to exceed the limit
   
   This scans every document before extraction on Spark 3.5+, even when the 
entire input is at most 1,000 bytes and therefore cannot contain more than 
1,000 ASCII numeric digits.
   
   For a representative 198-byte record with path `$.name`, an independent 
five-round alternating exact-source benchmark measured 338.54 ns on base versus 
547.29 ns on head. Adding only `json_str.len() > 1000` before this scan reduced 
the result to 356.60 ns, with identical outputs. Two separate seven-round runs 
corroborated the slowdown. Paths were parsed once and inputs/results were 
black-boxed; these are evaluator timings, not whole-query timings.
   
   Could we add this length guard? Serde still validates the document's syntax. 
The earlier bytewise 64 KiB string-scan issue is fixed; this is remaining 
avoidable work on ordinary short records.



##########
native/spark-expr/src/string_funcs/get_json_object.rs:
##########
@@ -246,68 +306,269 @@ fn parse_json_path(path: &str) -> Option<ParsedPath> {
         }
     }
 
-    Some(ParsedPath {
-        segments,
-        has_wildcard,
-    })
+    Some(ParsedPath { segments })
+}
+
+/// Find the end of a string body starting at `i` (just past the opening
+/// quote). Short bodies are scanned inline; long bodies use memchr2, the same
+/// approach as serde_json's `ignore_str`. Returns the index just past the
+/// closing quote, or None for an unterminated string (the parser rejects the
+/// document anyway).
+#[inline]
+fn skip_string_body(bytes: &[u8], mut i: usize) -> Option<usize> {
+    const SHORT_STRING: usize = 32;
+    if bytes.len().checked_sub(i)? <= SHORT_STRING {
+        while i < bytes.len() {
+            match bytes[i] {
+                b'"' => return Some(i + 1),
+                b'\\' => i = i.checked_add(2)?,
+                _ => i += 1,
+            }
+        }
+        return None;
+    }
+    loop {
+        match memchr::memchr2(b'"', b'\\', bytes.get(i..)?) {
+            Some(off) if bytes[i + off] == b'"' => return Some(i + off + 1),
+            Some(off) => i = i.checked_add(off)?.checked_add(2)?, // escaped 
byte
+            None => return None,
+        }
+    }
+}
+
+/// Spark 3.5+ bundles Jackson versions that reject numbers beyond the default
+/// 1000-digit limit, including values this evaluation skips. serde_json's
+/// `IgnoredAny` enforces no such limit, so inspect number tokens before 
parsing.
+/// Spark 3.4's Jackson has no default limit and bypasses this scan.
+fn has_oversized_number(json: &str) -> bool {
+    const MAX_NUMBER_DIGITS: usize = 1000;
+    const JACKSON_READER_BUFFER_UNITS: usize = 4000;
+    let bytes = json.as_bytes();
+    let mut i = 0;
+    // Only near-limit floats need the Reader's UTF-16 position. Keep the
+    // prefix count between candidates so many long numbers stay linear.
+    let mut counted_through = 0;
+    let mut utf16_units = 0;
+    while i < bytes.len() {
+        match bytes[i] {
+            // Skip string bodies: Jackson applies no numeric constraint to
+            // string content.
+            b'"' => match skip_string_body(bytes, i + 1) {
+                Some(end) => i = end,
+                None => return false,
+            },
+            b'-' | b'0'..=b'9' => {
+                let mut j = i + usize::from(bytes[i] == b'-');
+                let int_start = j;
+                while j < bytes.len() && bytes[j].is_ascii_digit() {
+                    j += 1;
+                }
+                let int_len = j - int_start;
+                let mut fract_len = 0;
+                if j < bytes.len() && bytes[j] == b'.' {
+                    j += 1;
+                    let start = j;
+                    while j < bytes.len() && bytes[j].is_ascii_digit() {
+                        j += 1;
+                    }
+                    fract_len = j - start;
+                }
+                let mut exp_len = 0;
+                if j < bytes.len() && (bytes[j] | 0x20) == b'e' {
+                    j += 1;
+                    if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == 
b'-') {
+                        j += 1;
+                    }
+                    let start = j;
+                    while j < bytes.len() && bytes[j].is_ascii_digit() {
+                        j += 1;
+                    }
+                    exp_len = j - start;
+                }
+                let is_float = fract_len > 0 || exp_len > 0;
+                let digit_count = if is_float {
+                    // Spark parses UTF8String via InputStreamReader, then
+                    // ReaderBasedJsonParser with a 4000-UTF-16-unit buffer.
+                    // Its slow path uses -1 for an absent fraction or 
exponent,
+                    // forgiving one digit when a number reaches a buffer edge
+                    // or EOF. Numbers starting with zero always take that 
path.
+                    let starts_with_zero = int_len == 1 && bytes[int_start] == 
b'0';
+                    let reaches_buffer_edge = if int_len + fract_len + exp_len 
> MAX_NUMBER_DIGITS {
+                        utf16_units += 
json[counted_through..i].encode_utf16().count();
+                        counted_through = i;
+                        utf16_units % JACKSON_READER_BUFFER_UNITS + (j - i)
+                            >= JACKSON_READER_BUFFER_UNITS
+                    } else {
+                        false
+                    };
+                    let slow_path = starts_with_zero || reaches_buffer_edge || 
j == bytes.len();
+                    let absent_component = fract_len == 0 || exp_len == 0;
+                    int_len + fract_len + exp_len - usize::from(slow_path && 
absent_component)
+                } else {
+                    int_len
+                };
+                if digit_count > MAX_NUMBER_DIGITS {
+                    return true;
+                }
+                i = j;
+            }
+            _ => i += 1,
+        }
+    }
+    false
 }
 
 /// Evaluate a parsed JSONPath against a JSON string.
 /// Returns the result as a string, or None if no match.
+#[cfg(test)]
 fn evaluate_path(json_str: &str, path: &ParsedPath) -> Option<String> {
-    if !path.has_wildcard {
-        return value_into_string(extract_no_wildcard(json_str, 
&path.segments)?);
-    }
-
-    let value: Value = serde_json::from_str(json_str).ok()?;
+    evaluate_path_with_number_limit(json_str, path, true)
+}
 
-    // Wildcard path: may return multiple results
-    let results = evaluate_with_wildcard(&value, &path.segments);
+fn evaluate_path_with_number_limit(
+    json_str: &str,
+    path: &ParsedPath,
+    check_number_length: bool,
+) -> Option<String> {
+    if check_number_length && has_oversized_number(json_str) {
+        return None;
+    }
 
-    match results.len() {
-        0 => None,
-        1 => {
-            // Single wildcard match: Spark preserves JSON serialization format
-            // (strings keep their quotes, numbers don't)
-            if results[0].is_null() {
-                None
-            } else {
-                serde_json::to_string(results[0]).ok()
-            }
-        }
-        // Multiple results: wrap in JSON array. A slice of `&Value` serializes
-        // as a JSON array, so no clone into an owned `Value::Array` is needed.
-        _ => serde_json::to_string(&results).ok(),
+    let result = extract_path(json_str, &path.segments)?;
+    if !result.matched {
+        return None;
     }
+    // The top level is not an array context. Jackson's generator separates
+    // consecutive root-level writes with a single space, so join with one.
+    Some(PathResult::join(result.writes, " "))
 }
 
-/// Evaluation for paths without wildcards.
-///
 /// Descends into the document while it is being parsed, so only the matched
-/// subtree is materialized as a `Value`; everything else is skipped by the
-/// parser without allocating. The whole document is still consumed, so
-/// malformed JSON anywhere in the input yields no match, as a full parse 
would.
-fn extract_no_wildcard(json_str: &str, segments: &[PathSegment]) -> 
Option<Value> {
+/// subtrees are materialized as `Value`s; everything else is skipped by the
+/// parser without allocating. The whole document is still consumed, so 
malformed
+/// JSON anywhere in the input yields no match, as a full parse would.
+fn extract_path(json_str: &str, segments: &[PathSegment]) -> 
Option<PathResult> {
     let mut de = serde_json::Deserializer::from_str(json_str);
-    let found = PathSeed { segments }.deserialize(&mut de).ok()?;
+    let found = PathSeed {
+        segments,
+        style: Style::Raw,
+        reject_direct_null: false,
+    }
+    .deserialize(&mut de)
+    .ok()?;
     de.end().ok()?;
-    found
+    Some(found)
 }
 
 /// Deserializes the value at `segments`, discarding everything else.
 struct PathSeed<'a> {
     segments: &'a [PathSegment],
+    /// The output style in effect, mirroring the `style` parameter Spark
+    /// threads through `evaluatePath`.
+    style: Style,
+    /// A JSON null directly below a named field is not a match in Spark. Nulls
+    /// reached through array traversal are matches and serialize as `null`.
+    reject_direct_null: bool,
+}
+
+/// The outcome of applying (part of) a path, modeled on Spark's generator
+/// protocol: `writes` holds one rendered fragment per generator write and
+/// `matched` is Spark's dirty flag.
+///
+/// The two can diverge: the wildcard arms that write directly to the generator
+/// emit their array wrapper even when nothing inside matched, so an unmatched
+/// result can still carry writes. Spark's generator keeps those bytes — a 
later
+/// occurrence of a duplicated field can build on them — so they are preserved
+/// here rather than discarded.
+#[derive(Default)]
+struct PathResult {
+    // A simple field or index lookup produces one write; keep it inline while
+    // descending through nested objects and arrays.
+    writes: SmallVec<[String; 1]>,
+    matched: bool,
+}
+
+impl PathResult {
+    fn join(mut writes: SmallVec<[String; 1]>, separator: &str) -> String {
+        if writes.len() == 1 {
+            writes.pop().unwrap()
+        } else {
+            writes.join(separator)
+        }
+    }
+
+    /// A single verbatim write of a matched value, honoring the output style:
+    /// a string in Raw style is written unquoted (Spark's scalar-unwrap arm),
+    /// everything else keeps JSON serialization.
+    fn write(value: Value, style: Style) -> Self {
+        match value {
+            Value::String(s) if style == Style::Raw => Self {
+                writes: smallvec![s],
+                matched: true,
+            },
+            value => Self {
+                writes: smallvec![value.to_string()],

Review Comment:
   [P2] Avoid per-leaf temporary serialization for dense wildcard results
   
   Each selected wildcard leaf is serialized into its own String here, then 
accumulated and copied into the wrapper at line 528. Repeated alternating 
exact-source benchmarks against the PR base show 1,000-number wildcard 
extraction at about 1.8-2.1x base runtime, and 1,000-string extraction at 
1.65-1.69x. An independent five-round check measured 54.02 us to 99.57 us for 
numbers and 94.23 us to 157.10 us for strings. The regression persists with 
numeric validation disabled, so it is separate from the pre-scan issue.
   
   The singleton ownership fix works, and numeric-wildcard allocation counts 
have improved. This finding concerns the remaining serialization/copy CPU cost. 
Sparse wildcard objects improve through selective traversal, so keeping that 
behavior matters.
   
   Could we add a common terminal-wildcard path that writes directly into an 
output buffer while preserving the existing per-level styles, match counts, and 
duplicate-field side effects? These measurements use cached parsed paths and 
black-boxed inputs/results and establish evaluator regressions, not whole-query 
timings.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to